Skip to content
← All docs

OAuth 2.1 + Dynamic Client Registration

How Niyra implements OAuth 2.1 for third-party MCP clients — DCR, PKCE, RFC 8707 audience binding, refresh rotation.

OAuth 2.1 + Dynamic Client Registration

Niyra implements the OAuth 2.1 authorization-server role for third-party clients — Claude Desktop, Cursor, ChatGPT, anything that wants to call /mcp or /v1/public/* on behalf of a Niyra user.

If you don't need OAuth (you only need your own scripts to call Niyra), use Personal Access Tokens instead.

Discovery

GET https://api.niyra.ai/.well-known/oauth-authorization-server

Returns the standard RFC 8414 metadata document, including:

  • authorization_endpoint/oauth/authorize
  • token_endpoint/oauth/token
  • registration_endpoint/oauth/register
  • revocation_endpoint/oauth/revoke
  • introspection_endpoint/oauth/introspect
  • jwks_uri/.well-known/jwks.json
  • scopes_supported — full catalog
  • code_challenge_methods_supported["S256"]

Step 1 — Register your client (DCR, RFC 7591)

curl -X POST https://api.niyra.ai/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My App",
    "redirect_uris": ["https://myapp.example.com/oauth/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "niyra:ask niyra:execute"
  }'

Response:

{
  "client_id": "niyra_abc123…",
  "client_id_issued_at": 1717100000,
  "client_name": "My App",
  "redirect_uris": ["https://myapp.example.com/oauth/callback"],
  "scope": "niyra:ask niyra:execute"
}

Public clients (CLIs, desktop apps, mobile) MUST use token_endpoint_auth_method: none and PKCE.

Step 2 — Authorization code with PKCE

Generate a code verifier + challenge:

const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));

Send the user to:

https://api.niyra.ai/oauth/authorize
  ?response_type=code
  &client_id=niyra_abc123
  &redirect_uri=https://myapp.example.com/oauth/callback
  &scope=niyra:ask%20niyra:execute
  &state=<random>
  &code_challenge=<challenge>
  &code_challenge_method=S256
  &resource=https://api.niyra.ai/mcp

The resource parameter is RFC 8707 — it binds the resulting access token to a specific audience.

After consent, Niyra redirects to:

https://myapp.example.com/oauth/callback?code=<auth_code>&state=<state>

Step 3 — Exchange code for tokens

curl -X POST https://api.niyra.ai/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<auth_code>" \
  -d "client_id=niyra_abc123" \
  -d "redirect_uri=https://myapp.example.com/oauth/callback" \
  -d "code_verifier=<verifier>" \
  -d "resource=https://api.niyra.ai/mcp"

Response:

{
  "access_token": "eyJhbGc…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rft_xyz…",
  "scope": "niyra:ask niyra:execute"
}

Step 4 — Use the token

curl https://api.niyra.ai/v1/public/ask \
  -H "Authorization: Bearer eyJhbGc…" \
  -d '{"question": "What is on my calendar?"}'

Refresh rotation

Refresh tokens rotate on every use — the response includes a new refresh token and the old one is marked rotated. Replaying a rotated refresh token triggers OAuth 2.1 §6.1 cascade revocation: the entire token chain is invalidated, so a stolen refresh can be used at most once before the legitimate client discovers the theft.

curl -X POST https://api.niyra.ai/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=rft_xyz…" \
  -d "client_id=niyra_abc123"

Revoking tokens

curl -X POST https://api.niyra.ai/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=<access_or_refresh>" \
  -d "client_id=niyra_abc123"

Per RFC 7009, the endpoint always returns 200, regardless of whether the token existed.

Related

FAQ

Do I need to register my app?
Yes — but it's automatic. POST your client metadata to /oauth/register and you get a client_id back. No manual review, no waiting list.
Why RFC 8707 resource indicators?
They bind each token to a specific audience (api.niyra.ai/mcp), so a token can't be aimed at a different surface if it leaks.
For AI:.md.txt