OAuth 2.1 with PKCE
AINative Studio implements an OAuth 2.1 authorization server with three components:
- Dynamic Client Registration (RFC 7591) -- programmatic registration of OAuth clients
- Authorization Code Flow with PKCE (RFC 7636) -- secure token issuance for public clients (CLIs, SPAs)
- Token Management -- exchange, refresh, and revocation
PKCE uses the S256 challenge method exclusively. The plain method is not supported.
1. Dynamic Client Registration
Endpoint: POST /oauth/clients/register
Implements RFC 7591. Allows MCP servers and other OAuth clients to register without manual configuration.
Request
{
"client_name": "My MCP Server",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "memory:read memory:write",
"token_endpoint_auth_method": "none",
"client_uri": "https://example.com",
"logo_uri": "https://example.com/logo.png",
"contacts": ["dev@example.com"],
"tos_uri": "https://example.com/tos",
"policy_uri": "https://example.com/privacy"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
client_name | string | Yes | -- | Human-readable name (1-255 chars) |
redirect_uris | string[] | No | [] | Redirect URIs (must use http or https) |
grant_types | string[] | No | ["authorization_code"] | Supported: authorization_code, refresh_token, client_credentials |
response_types | string[] | No | ["code"] | Supported: code |
scope | string | No | "" | Space-separated scopes |
token_endpoint_auth_method | string | No | "client_secret_basic" | One of: client_secret_basic, client_secret_post, none |
client_uri | string | No | null | Client home page URL |
logo_uri | string | No | null | Client logo URL |
contacts | string[] | No | null | Contact email addresses |
tos_uri | string | No | null | Terms of service URL |
policy_uri | string | No | null | Privacy policy URL |
Available Scopes
| Scope | Description |
|---|---|
memory:read | Read from memory/vector stores |
memory:write | Write to memory/vector stores |
vector:search | Search vector embeddings |
vector:write | Write vector embeddings |
agent:read | Read agent state and history |
agent:write | Create and manage agents |
openid | OpenID Connect identity |
profile | User profile information |
email | User email address |
Response (201 Created)
{
"client_id": "a1b2c3d4-...",
"client_secret": "dGhpcyBpcyBhIHNlY3JldA...",
"client_name": "My MCP Server",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "memory:read memory:write",
"token_endpoint_auth_method": "none",
"client_uri": "https://example.com",
"logo_uri": "https://example.com/logo.png",
"contacts": ["dev@example.com"],
"tos_uri": "https://example.com/tos",
"policy_uri": "https://example.com/privacy",
"created_at": "2026-06-01T12:00:00+00:00"
}
Store client_id and client_secret securely. The secret is generated once and cannot be retrieved again.
2. Authorization Code Flow with PKCE
Step 1: Generate PKCE Code Verifier and Challenge
The client generates a cryptographically random code verifier (43-128 URL-safe characters) and derives the S256 challenge:
import secrets
import hashlib
import base64
# Generate verifier (43-128 chars)
code_verifier = secrets.token_urlsafe(64)[:64]
# Derive S256 challenge: BASE64URL(SHA256(verifier))
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
Step 2: Authorization Request
Redirect the user to the authorization endpoint:
GET /oauth/authorize
?response_type=code
&client_id=<client_id>
&redirect_uri=http://localhost:8080/callback
&scope=memory:read memory:write
&state=<random_csrf_token>
&code_challenge=<code_challenge>
&code_challenge_method=S256
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | From client registration |
redirect_uri | Yes | Must match a registered URI or the static allowlist |
scope | Yes | Space-separated scopes |
state | Recommended | CSRF protection; returned unchanged in the callback |
code_challenge | Yes | S256 PKCE challenge |
code_challenge_method | Yes | Must be S256 |
Redirect URI allowlist (static fallback):
http://localhost:*-- CLI automatic flowhttp://127.0.0.1:*-- CLI automatic flowhttps://*.ainative.studio/*-- first-party apps
If the client is registered in oauth_clients, its registered redirect_uris are checked by exact match instead.
The endpoint renders a login page. After the user authenticates, the server redirects to the redirect_uri with an authorization code:
http://localhost:8080/callback?code=<auth_code>&state=<state>
Authorization codes expire after 5 minutes and are single-use.
Step 3: Token Exchange
Exchange the authorization code for tokens. Accepts both form-encoded and JSON request bodies.
POST /v1/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<auth_code>
&redirect_uri=http://localhost:8080/callback
&client_id=<client_id>
&code_verifier=<code_verifier>
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | authorization_code |
code | Yes | Authorization code from step 2 |
redirect_uri | Yes | Must match the authorize request |
client_id | Yes | Must match the authorize request |
code_verifier | Yes | PKCE verifier from step 1 |
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "memory:read memory:write"
}
The server verifies the code verifier against the stored challenge using constant-time comparison (HMAC) to prevent timing attacks.
3. Token Refresh
Exchange a refresh token for a new access token. Refresh tokens are rotated on each use (the old token is invalidated).
POST /v1/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "bmV3IHJlZnJlc2ggdG9r...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "memory:read memory:write"
}
4. Token Revocation
Revoke an access or refresh token (RFC 7009). Always returns 200 even for invalid tokens, per the specification.
POST /v1/oauth/revoke
Content-Type: application/json
{
"token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"token_type_hint": "refresh_token"
}
| Field | Required | Description |
|---|---|---|
token | Yes | The token to revoke |
token_type_hint | No | Optional hint: access_token or refresh_token |
Response (200 OK):
{
"status": "ok"
}
Token Lifetimes
| Token Type | Lifetime |
|---|---|
| Authorization code | 5 minutes |
| Access token (JWT) | 1 hour |
| Refresh token | 30 days |
Access tokens are HS256-signed JWTs with the following claims:
| Claim | Description |
|---|---|
sub | User email or identifier |
scope | Space-separated granted scopes |
type | oauth_access |
exp | Expiration timestamp |
iat | Issued-at timestamp |
OAuth Scopes (Authorization Server)
The authorization server recognizes these scopes at the /oauth/authorize endpoint:
| Scope | Description |
|---|---|
user:profile | Read user profile |
user:inference | Run inference requests |
user:sessions | Manage chat sessions |
user:sessions:claude_code | CLI-specific session scope |
user:mcp_servers | Manage MCP server connections |
user:file_upload | Upload files |
org:create_api_key | Create API keys via OAuth (CLI Console) |
openid | OpenID Connect identity |
profile | OIDC profile claims |
email | OIDC email claim |
memory:read | Read memory/vectors |
memory:write | Write memory/vectors |
vector:search | Search vector embeddings |
vector:write | Write vector embeddings |
agent:read | Read agent state |
agent:write | Manage agents |
Complete CLI Flow Example (Python)
import secrets
import hashlib
import base64
import webbrowser
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import requests
# 1. Register client (one-time)
reg = requests.post("https://api.ainative.studio:8000/oauth/clients/register", json={
"client_name": "my-cli",
"redirect_uris": ["http://localhost:9876/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"scope": "user:profile user:inference",
}).json()
client_id = reg["client_id"]
# 2. Generate PKCE pair
verifier = secrets.token_urlsafe(64)[:64]
digest = hashlib.sha256(verifier.encode("ascii")).digest()
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
state = secrets.token_urlsafe(16)
# 3. Open browser for authorization
auth_url = (
f"https://api.ainative.studio:8000/oauth/authorize"
f"?response_type=code&client_id={client_id}"
f"&redirect_uri=http://localhost:9876/callback"
f"&scope=user:profile user:inference"
f"&state={state}&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
webbrowser.open(auth_url)
# 4. Receive callback on local server
auth_code = None
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
nonlocal auth_code
params = parse_qs(urlparse(self.path).query)
auth_code = params["code"][0]
self.send_response(200)
self.end_headers()
self.wfile.write(b"Login successful. You can close this window.")
server = HTTPServer(("localhost", 9876), Handler)
server.handle_request()
# 5. Exchange code for tokens
tokens = requests.post("https://api.ainative.studio:8000/v1/oauth/token", data={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": "http://localhost:9876/callback",
"client_id": client_id,
"code_verifier": verifier,
}).json()
print(f"Access token: {tokens['access_token'][:20]}...")
Error Responses
| Status | Endpoint | Condition |
|---|---|---|
| 400 | /oauth/authorize | Invalid response_type, code_challenge_method, redirect URI, or scopes |
| 400 | /v1/oauth/token | Invalid or expired code, client_id mismatch, PKCE failure, unsupported grant_type |
| 400 | /v1/oauth/token | Invalid or expired refresh token |
| 422 | /oauth/clients/register | Validation error on client metadata |
| 500 | /oauth/clients/register | Database initialization or persistence failure |