---
title: OAuth 2.1 + Dynamic Client Registration
description: How Niyra implements OAuth 2.1 for third-party MCP clients — DCR, PKCE, RFC 8707 audience binding, refresh rotation.
url: /docs/api-oauth
lastUpdated: 2026-06-11
---

# OAuth 2.1 + Dynamic Client Registration


# 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](/docs/api-tool-niyra-ask) instead.

## Discovery

```http
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)

```bash
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:

```json
{
  "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:

```js
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

```bash
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:

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

## Step 4 — Use the token

```bash
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.

```bash
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

```bash
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

- [Scope catalog](/docs/api-scopes)
- [Rate limits](/docs/api-rate-limits)
