> ## Documentation Index
> Fetch the complete documentation index at: https://noorle.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# JWT Tokens

> The three token issuers on Noorle, the claims each one signs, and why only one of them publishes a JWKS

Noorle issues JWTs from three places. They look alike on the wire and are not interchangeable. Knowing which one you are holding answers most questions about why a token was rejected.

|                | Identity boundary        | Platform                                  | Each MCP gateway                      |
| -------------- | ------------------------ | ----------------------------------------- | ------------------------------------- |
| Host           | `auth.noorle.com`        | `api.noorle.com`                          | `mcp-{handle}.noorle.com`             |
| Token          | `id_token`               | access token                              | access token                          |
| Algorithm      | **RS256**                | **HS256**                                 | **HS256**                             |
| Key published? | **Yes** — JWKS endpoint  | No                                        | No                                    |
| Lifetime       | 5 minutes                | 1 hour                                    | See the gateway                       |
| Answers        | "Who is this principal?" | "May this caller use the management API?" | "May this caller use *this* gateway?" |

## Why only one publishes a JWKS

RS256 is asymmetric: the boundary signs with a private key and publishes the public half, so anyone can verify an `id_token` without holding a secret. That is what makes it usable as an identity assertion across services.

HS256 is symmetric — the signing key and the verifying key are the same secret. Publishing a JWKS for a platform or gateway token would mean publishing the signing secret, so those surfaces deliberately advertise `jwks_uri: null`. Verification happens inside Noorle.

Do not write a client that expects to verify a platform or gateway token itself. It cannot, by design.

## The identity `id_token`

Issued by the boundary at the end of a sign-in, exchanged back-channel with `client_secret_basic`.

```json theme={null}
{
  "iss": "https://auth.noorle.com",
  "sub": "<principal id>",
  "aud": "<client id>",
  "iat": 1711179600,
  "exp": 1711179900,
  "email": "alice@example.com",
  "email_verified": true,
  "auth_time": 1711175000,
  "principal_type": "human"
}
```

`principal_type` is Noorle's one custom claim: `human`, `service`, or `agent`. `sub` is the principal id, and it is the *same* id the platform uses for that identity — there is no mapping table between the two databases.

Additional claims may be added over time. A validator must not reject a token for carrying a claim it does not recognize.

<Warning>
  The boundary's `/token` response also carries an `access_token`. It is an inert random placeholder with no meaning and no accepted use. The `id_token` is the only carrier of identity in that response.
</Warning>

### Key rotation

The boundary can hold several signing keys at once. The **first** signs; **all** of them publish in the JWKS, and each key's `kid` is derived from its own material. Rotation is therefore a three-step sequence — add the new key second, promote it to first, then drop the old — so no token is ever in flight signed by a key that is not published.

## The platform access token

Issued by the CLI [device flow](/docs/learn/auth/oauth-device-flow), signed HS256.

Claims include `iss` (the management-API host that served it), `sub` (the approving user), `aud`, `exp`, `iat`, `scope`, `account_id`, and a token type. An agent's outbound token additionally carries a delegation chain, where the agent is the immediate actor and whoever triggered the run is preserved as a nested claim.

**Scope** is `account:manage:{account_id}`. It names an account, not a permission set — authority still comes from the principal's role and grants. There are no `read` / `write` / `admin` / `offline_access` scopes on Noorle.

Lifetime is one hour.

## Gateway tokens

Each MCP gateway is its own authorization server. A token it mints is admitted only on that gateway, and only if it carries the `mcp` scope, matches the gateway's account, and lists that gateway's resource URN in its audience.

There is no cross-gateway fallback: one gateway's token is simply not a credential anywhere else.

A gateway advertises `authorization_code`, `refresh_token`, and `client_credentials`, with PKCE S256 only.

## Delegation chains

When an agent makes an outbound call, the token records both who is acting and who caused it. Every subject in the chain must resolve to a live principal in the same account, and the chain is capped at **5** levels. The effective caller — the outermost delegator, or the subject if there is none — is what authorization and audit attribute the call to.

This is why a denial can match an identity you did not expect: a deny that names the human who started a run also applies to the sub-agent acting on their behalf.

## Practical guidance

* **Do not decode a token and trust the payload.** Anyone can write claims; only the signature makes them true.
* **Do not try to verify a Noorle HS256 token client-side.** You do not have the key, and you should not.
* **Send tokens only over HTTPS**, and keep access tokens out of URLs, logs, and browser local storage.
* **Watch `exp` and re-authenticate before it passes**, rather than discovering it through a 401 mid-operation.

## Troubleshooting

| Symptom                                          | Likely cause                                                                                                                   |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| 401 on the management API                        | The one-hour access token expired. Re-run the device flow.                                                                     |
| Gateway rejects a valid-looking token            | It was minted by a different gateway, or its audience does not carry this gateway's resource URN, or it lacks the `mcp` scope. |
| A boundary `access_token` does not work anywhere | Expected. It is a placeholder; use the `id_token`.                                                                             |
| Signature verification fails in your own code    | If it is an HS256 platform or gateway token, you cannot verify it — there is no published key.                                 |

***

Next: [Connected Apps](/docs/learn/auth/connected-apps).
