> Source: https://wexa.ai/docs/surfaces/rest/authentication

# REST authentication

Every authenticated route takes one header:

```
Authorization: Bearer <credential>
```

The gateway accepts four kinds of credential behind that one header. They are not
interchangeable — each is minted differently, carries a different scope, and can reach a different
set of routes. Picking the wrong one produces a `401` that looks like a bad secret when it is
really the wrong *kind* of secret.

## The four types

| | API key | User token | OAuth 2.1 access token | SDK session |
|---|---|---|---|---|
| Looks like | `fab_sk_…` | A JWT | A JWT | A JWT, plus a `fab_srt_…` refresh token |
| Minted by | `POST /v1/apikeys` | Sign-in | The `/oauth/authorize` + `/oauth/token` exchange | [`POST /v1/auth/login`](/docs/tools/login) with email and password |
| Scope | Pinned at mint time to one organization, department and project | The session's own scope | The scopes the person consented to | One project, as the logged-in user |
| Lifetime | Until revoked | The session's expiry | The token's expiry, refreshable | One hour, refreshable for 30 days |
| Reaches tool routes | Yes | Yes | Yes | Yes |
| Reaches `/v1/apikeys` | **No** | Yes, with an admin role | Depends on the granted scopes | **No** |
| Right for | Servers, jobs, anything unattended | A person's own session, and credential management | An application acting for a person | Your own login page, acting as the person who logged in |

The gateway records which one you used. `POST /v1/docs` reports it as `auth_kind`, and the value is
`api_key`, `user_jwt` or `session` — useful when a call behaves differently from how you expected and you are
not certain which credential your client picked up.

## API keys — the default for anything unattended

An API key is the credential to reach for from a server, a scheduled job, or a developer machine.
Its scope is fixed when it is minted and never widens, which is what makes it safe to hand to a
process: a key cannot switch project, and a key cannot mint another key.

### Minting one

Key creation requires a **user token with an admin role on the target project** — `OWNER`,
`ORG_ADMIN` or `PROJECT_ADMIN`. A lower role is refused with
`API key creation requires an admin role on this project`, and an API key presented here is refused
outright with `401 token invalid`. That refusal is deliberate: if a key could mint a key, revoking
the first one would not contain the damage.

```bash
curl -s -X POST "$BASE/v1/apikeys" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"docs-c10","org_id":"org_c10","dept_id":"dept_c10","project_id":"proj_c10"}'
```

The response carries the secret exactly once:

```json
{
  "id": "key_jFN-7B_Qvj9JYz7FP2V_-4fi",
  "name": "docs-c10",
  "note": "store this secret now; it is not retrievable again",
  "scope": {
    "org_id": "org_c10", "dept_id": "dept_c10", "project_id": "proj_c10",
    "user_id": "u_c10", "role": "OWNER",
    "grants": ["fabric:query.read", "fabric:docs.read", "fabric:ontology.write",
               "fabric:orchestrate.read", "fabric:orchestrate.write", "fabric:agent.run",
               "fabric:skill.write", "fabric:model.write"],
    "auth_kind": "api_key"
  },
  "secret": "fab_sk_…"
}
```

### Grants

When you request no explicit grants, the key gets the default set for the minter's role. An admin
role yields the eight-grant set shown above. Any other role — or an unrecognised one — fails closed
to the read-only pair, `fabric:query.read` and `fabric:docs.read`.

Explicit grants still win, so a deliberately narrow key stays narrow. A key can never do anything
its minter could not already do.

Call `POST /v1/docs` with `{"topic":"governance"}` to see the grants a credential actually holds
rather than the ones you intended it to hold.

### Listing and revoking

Both take `project_id` as a query parameter and both require a user token with an admin role on that
project.

```bash
curl -s "$BASE/v1/apikeys?project_id=proj_c10" -H "Authorization: Bearer $USER_TOKEN"
curl -s -X DELETE "$BASE/v1/apikeys/key_jFN-7B_Qvj9JYz7FP2V_-4fi?project_id=proj_c10" \
  -H "Authorization: Bearer $USER_TOKEN"
```

Revocation is immediate. The next call with that key gets
`401 {"error":"invalid_token","error_description":"api key invalid or revoked"}`.

## User tokens

A user token is a signed JWT carrying the caller's identity and scope: `sub`, `role`, `org_id`,
`dept_id`, `project_id`, and `token_use`. It is what a signed-in person's own session holds.

It reaches tool routes as well as credential management. Its grants come from its own claims, or
from the default set for its role when it carries none — which means a user token and an API key
minted by the same person can end up with *different* effective grants. Check with
`POST /v1/docs` rather than assuming.

Use a user token for credential management and for a person acting as themselves. Do not embed one
in a service: it expires with the session, and its scope is the person's rather than the job's.

## OAuth 2.1

OAuth is the path for an application acting on a person's behalf, including an editor or desktop
assistant connecting over the Wexa MCP server. The gateway is both the resource server and its own
authorization server, and it publishes two discovery documents so a client can configure itself:

```bash
curl -s "$BASE/.well-known/oauth-protected-resource"
curl -s "$BASE/.well-known/oauth-authorization-server"
```

The authorization-server document reports:

```json
{
  "issuer": "http://localhost:7123",
  "authorization_endpoint": "http://localhost:7123/oauth/authorize",
  "token_endpoint": "http://localhost:7123/oauth/token",
  "registration_endpoint": "http://localhost:7123/oauth/register",
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "response_types_supported": ["code"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"]
}
```

Authorization code with PKCE (`S256`) is the only code flow offered, and dynamic client registration
is available at `/oauth/register`, so a client can onboard without anyone pre-registering it.

The scopes both documents advertise are the grant names themselves:

```
fabric:query.read   fabric:ontology.write   fabric:docs.read   fabric:codesync.write
fabric:agent.run    fabric:orchestrate.read fabric:orchestrate.write
```

## What a missing or bad credential looks like

Sending nothing:

```text
HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer resource_metadata="http://localhost:7123/.well-known/oauth-protected-resource"

{"error":"invalid_token","error_description":"missing bearer credential"}
```

The `WWW-Authenticate` header points at the protected-resource document, so a compliant client can
discover where to authenticate without being told. Sending a bad or revoked key gives the same
status with `api key invalid or revoked`; sending a credential of the wrong kind for the route gives
`token invalid`.

Both SDKs map every one of these to `AuthError`. See
[error handling](/docs/surfaces/typescript/error-handling) for the rest of the hierarchy.

## Environment variables

Both SDKs read the same two variables, and using the same names for a `curl` session keeps every
sample on this site copy-pasteable:

```bash
export WEXA_WORKSPACE=https://fabric.wexa.ai
export WEXA_API_KEY=fab_sk_…
```

`WEXA_WORKSPACE` is the workspace URL, not the API base. The API base comes from
`GET /v1/connection-info`.

## Related

  <Card title="REST overview" href="/docs/surfaces/rest/overview">
    The tool-call convention and the non-tool routes.
  </Card>
  <Card title="Errors and retries" href="/docs/surfaces/rest/errors-and-retries">
    Every status code this surface returns, and what to do about it.
  </Card>
  <Card title="Tenancy" href="/docs/concepts/tenancy">
    Organization, department and project — what a credential's scope pins.
  </Card>
  <Card title="Policy and approvals" href="/docs/concepts/policy-and-approvals">
    What happens after a credential is accepted.
  </Card>
