> ## 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.

# Authentication

> How to authenticate the Noorle management API with API keys or the OAuth device flow, and how MCP and agent gateways authenticate callers

Each of Noorle's three surfaces authenticates differently. The management API
takes an API key or a device-flow JWT. Each MCP gateway is its own OAuth
authorization server. The agent gateway takes a bearer credential scoped to the
agent.

## Management API

`api.noorle.com` accepts a bearer credential on the `Authorization` header. It
never reads cookies — the surface is built for service accounts and CLI clients,
not a browser session.

### API keys

API keys are created in the [Portal](https://platform.noorle.com) under
**Settings → API Keys**. They begin with `ak-`.

```bash theme={null}
curl https://api.noorle.com/v1/capabilities \
  -H "Authorization: Bearer ak-YOUR_KEY"
```

Four header forms are accepted, checked in this order:

1. `Authorization: Bearer <credential>`
2. `Authorization: ApiKey <credential>`
3. `API-Key: <credential>`
4. `X-API-Key: <credential>`

Prefer `Authorization: Bearer` — it is the form the gateways also accept, so one
code path covers every surface. An API key and a device-flow JWT go in the same
slot; you do not tell the server which kind you are sending, and there is no
prefix rule to satisfy on this host.

An API key row carries an optional expiry and an optional revocation timestamp.
Both are checked before the secret is verified, so a revoked key fails
immediately regardless of whether the secret is correct.

<Warning>
  Treat an API key like a password. It is not scoped to a subset of operations —
  it carries the authority of the principal it belongs to. Store it in a secrets
  manager or an environment variable, never in source control.
</Warning>

### OAuth device flow

The device flow is how the Noorle CLI authenticates a human at a terminal. It is
served by the management API, and it is the **only** grant type
`POST /oauth/token` accepts on this host.

```mermaid theme={null}
sequenceDiagram
    participant C as CLI
    participant A as api.noorle.com
    participant U as You (browser)

    C->>A: POST /oauth/device/authorize
    A-->>C: device_code, user_code, verification_uri
    C->>U: Open verification_uri, enter user_code
    U->>A: Approve in the Portal
    loop every `interval` seconds
        C->>A: POST /oauth/token (device_code grant)
        A-->>C: still pending (HTTP 400)
    end
    A-->>C: access_token
```

```bash theme={null}
# 1. Request a device code
curl -X POST https://api.noorle.com/oauth/device/authorize \
  -H "Content-Type: application/json" \
  -d '{"client_id": "noorle-cli"}'

# 2. Approve at the returned verification_uri, then poll:
curl -X POST https://api.noorle.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "code": "dc_...",
    "client_id": "noorle-cli"
  }'
```

<Note>
  The device code goes in a request field named **`code`**, not `device_code`.
  The *response* field from `/oauth/device/authorize` is named `device_code`;
  the field you send it back in is `code`.
</Note>

The access token is a JWT valid for **1 hour**. Use it as a bearer credential
exactly like an API key.

Both device-flow responses come back **bare** — they are not wrapped in the
`data` envelope the `/v1` endpoints use. Read their fields off the top level.

<Warning>
  **There is no refresh flow.** The device-code grant is the only grant this
  host accepts, so an expired token cannot be exchanged for a fresh one. Run the
  device flow again, or use an API key for unattended clients.
</Warning>

See [Device Authorization](/docs/reference/rest/oauth-device-authorize) and
[Get Token](/docs/reference/rest/oauth-token) for the full request and response
shapes.

### There is no revocation or introspection endpoint

`api.noorle.com` serves exactly two OAuth routes: `/oauth/device/authorize` and
`/oauth/token`. There is no `/oauth/token/revoke`, no `/oauth/introspect`, and no
discovery document. To cut off access, revoke the API key in the Portal or wait
out the one-hour token lifetime.

## MCP gateways

**Each MCP gateway origin is its own OAuth authorization server.** A token
minted for one gateway does not validate at another.

Gateways have three auth modes, set per gateway in the Portal:

| Mode                               | Behavior                                                            |
| ---------------------------------- | ------------------------------------------------------------------- |
| **Public**                         | No credential required                                              |
| **Private**                        | Credential required; clients must be registered out of band         |
| **Private + dynamic registration** | Credential required; clients may self-register at `/oauth/register` |

A bearer credential is classified by prefix — `ak-` is an API key, `eyJ` is a
JWT, anything else is rejected outright as an invalid token format.

```bash theme={null}
curl -X POST https://mcp-my-gateway.noorle.com/ \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

A JWT is admitted only when all four hold: the signature validates against
**that gateway's** issuer, the token carries the `mcp` scope, its `account_id`
matches the gateway's account, and its audience contains that gateway's resource
URN (`urn:noorle:mcp-gateway:{uuid}`). There is no cross-gateway fallback.

An API key is admitted when it is valid, belongs to the gateway's account, and
the principal holds read authority on the gateway.

### Discovery

```
GET https://mcp-my-gateway.noorle.com/.well-known/oauth-protected-resource
GET https://mcp-my-gateway.noorle.com/.well-known/oauth-authorization-server
```

The authorization-server document advertises:

| Field                                            | Value                                                           |
| ------------------------------------------------ | --------------------------------------------------------------- |
| `issuer`                                         | The gateway's own origin                                        |
| `authorization_endpoint`                         | `{origin}/oauth/authorize`                                      |
| `token_endpoint`                                 | `{origin}/oauth/token`                                          |
| `registration_endpoint`                          | `{origin}/oauth/register`                                       |
| `response_types_supported`                       | `["code"]`                                                      |
| `grant_types_supported`                          | `["authorization_code", "refresh_token", "client_credentials"]` |
| `token_endpoint_auth_methods_supported`          | `["client_secret_basic", "client_secret_post", "none"]`         |
| `code_challenge_methods_supported`               | `["S256"]` — `plain` is not offered                             |
| `scopes_supported`                               | `["mcp", "profile"]`                                            |
| `resource_indicator_param_supported`             | `true`                                                          |
| `authorization_response_iss_parameter_supported` | `true`                                                          |
| `response_modes_supported`                       | `["query", "fragment"]`                                         |

<Warning>
  **Unset fields are absent from the JSON, not present with a `null` value.**
  `jwks_uri`, `device_authorization_endpoint`, and
  `dpop_signing_alg_values_supported` do not appear in the document at all.
  Test for the key (`"jwks_uri" in metadata`) rather than comparing against
  `null`.
</Warning>

There is no JWKS to fetch: platform OAuth tokens are signed with a symmetric
HS256 key, and publishing a JWKS would mean publishing the signing secret.
Clients cannot verify these tokens locally; the gateway verifies them.

`/authorize`, `/token`, and `/register` are also served at the origin root as
aliases, for clients that skip metadata discovery.

A `401` from a gateway carries an RFC 9728 challenge pointing at the
protected-resource metadata:

```
WWW-Authenticate: Bearer resource_metadata="https://mcp-my-gateway.noorle.com/.well-known/oauth-protected-resource", error="invalid_token"
```

Once a request clears the gateway's auth mode — every request, whatever the
method, including an anonymous one to a public gateway — the gateway checks the
account's billing state. No available budget returns **402 Payment Required**; a
failure to determine it returns **503**.

## Agent gateways

The agent gateway takes one bearer credential, in two self-identifying kinds.

```bash theme={null}
curl -X POST https://agent-my-agent.noorle.com/ \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"GetTask","params":{"id":"..."}}'
```

**API keys may invoke an agent only as a service principal.** A user's API key is
rejected with 403. The service-principal path additionally requires that the
account matches, the principal is live and a member of the account, and it holds
execute authority on the agent — all fail-closed.

**JWTs** must carry `urn:noorle:agent:{id}` in the audience, every subject in the
delegation chain must resolve to a live principal in the same account, the chain
depth must be at most 5, and the effective invoking principal must be active.

The agent card at `GET /.well-known/agent-card.json` is deliberately public and
unauthenticated — it is the contract that tells a caller what credential to
attach. Everything else on the host requires one.

A `401` carries an RFC 6750 challenge:

```
WWW-Authenticate: Bearer realm="a2a", error="invalid_token"
```

## What is not supported anywhere

* **No query-parameter tokens.** `?access_token=` and `?token=` are not read on
  any surface.
* **No WebSocket.** Neither gateway exposes a WebSocket endpoint.
* **No session cookies** on the management API or either gateway.
* **No client-credentials or refresh grant on `api.noorle.com`.** Only the
  device-code grant.

## Troubleshooting

<AccordionGroup>
  <Accordion title="403 from the management API">
    The management API returns **403 for credential failures as well as
    permission failures** — a missing header, an invalid token, an expired
    token, and an unauthorized caller all produce 403. Read the `message` field
    to tell them apart.
  </Accordion>

  <Accordion title="'Invalid token format' from an MCP gateway">
    The gateway classifies a bearer credential by prefix. It must start with
    `ak-` (API key) or `eyJ` (JWT). Anything else is rejected before validation.
  </Accordion>

  <Accordion title="A gateway token stopped working after you created a second gateway">
    Tokens are per-gateway. Each origin is its own issuer and checks that its own
    resource URN is in the audience. Mint a token against the gateway you are
    calling.
  </Accordion>

  <Accordion title="406 from an MCP gateway POST">
    Streamable HTTP requires the request to accept both content types. Send
    `Accept: application/json, text/event-stream`.
  </Accordion>
</AccordionGroup>

## Next steps

* [Device Authorization](/docs/reference/rest/oauth-device-authorize)
* [Get Token](/docs/reference/rest/oauth-token)
* [Errors and rate limits](/docs/reference/errors-and-rate-limits)
