OAuth 2.0 PKCE flow, token exchange, session management, DCR, resource indicators
Overview
Fast.io implements OAuth 2.0 with PKCE (Proof Key for Code Exchange) for secure authorization without passing user credentials through the client application. This is the recommended auth method for desktop apps, mobile apps, and MCP-connected agents.
Key characteristics:
- S256 challenge method only (plain not supported)
- Access tokens last 1 hour (3600 seconds)
- Refresh tokens last 30 days
- Authorization codes last 5 minutes
- Authorization requests last 10 minutes
- Supports SSO (user signs in via browser, supports federated login)
- No user password passes through the agent/client
- Dynamic Client Registration (RFC 7591/7592) for automated client onboarding
- Client ID Metadata Document (CIMD) for URL-based client identification
- Resource Indicators (RFC 8707) for audience-restricted JWT access tokens
- Scoped access tokens (JWT v2.0) with entity-level restrictions and agent identity
- Metadata discovery (RFC 8414, RFC 9728) for automated server/resource configuration
Endpoint Summary
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /.well-known/oauth-authorization-server/ | None | RFC 8414 authorization server metadata |
| GET | /.well-known/oauth-protected-resource/ | None | RFC 9728 protected resource metadata |
| POST | /current/oauth/register/ | None | RFC 7591 dynamic client registration |
| PUT | /current/oauth/register/ | Registration access token (Bearer) | RFC 7592 update client registration |
| GET | /current/oauth/authorize/ | None | Initiate auth flow — 302 redirect to login page (or JSON with response_format=json) |
| POST | /current/oauth/authorize/ | Session (JWT) | Complete authorization, issue code |
| GET | /current/oauth/authorize/info/ | None | Get client info for consent screen |
| POST | /current/oauth/token/ | None | Exchange code for tokens, or refresh tokens |
| POST | /current/oauth/revoke/ | None | Revoke a refresh token |
| GET | /current/oauth/sessions/ | Bearer | List all active sessions |
| GET | /current/oauth/sessions/{id}/ | Bearer | Get session details |
| PATCH | /current/oauth/sessions/{id}/ | Bearer | Update session display names |
| DELETE | /current/oauth/sessions/{id}/ | Bearer | Revoke a specific session |
| DELETE | /current/oauth/sessions/ | Bearer | Revoke all sessions |
| GET | /current/auth/scopes/ | Bearer | Token scope introspection |
Metadata Discovery
GET /.well-known/oauth-authorization-server/
RFC 8414 Authorization Server Metadata. Returns server configuration for automated client setup.
/.well-known/oauth-authorization-server/
Auth: None · Rate Limit: 60/min, 600/hr per IP
curl -X GET "https://api.fast.io/.well-known/oauth-authorization-server/"
Response (200 OK):
{
"issuer": "https://api.fast.io",
"authorization_endpoint": "https://api.fast.io/current/oauth/authorize/",
"token_endpoint": "https://api.fast.io/current/oauth/token/",
"revocation_endpoint": "https://api.fast.io/current/oauth/revoke/",
"registration_endpoint": "https://api.fast.io/current/oauth/register/",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256"],
"resource_indicators_supported": true,
"service_documentation": "https://fast.io/docs"
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| issuer | string | Authorization server issuer identifier URL |
| authorization_endpoint | string | URL of the authorization endpoint |
| token_endpoint | string | URL of the token endpoint |
| revocation_endpoint | string | URL of the token revocation endpoint |
| registration_endpoint | string | URL of the dynamic client registration endpoint |
| response_types_supported | array | Supported response types (code only) |
| grant_types_supported | array | Supported grant types (authorization_code, refresh_token) |
| token_endpoint_auth_methods_supported | array | Supported auth methods (none — public clients only) |
| code_challenge_methods_supported | array | Supported PKCE methods (S256 only) |
| resource_indicators_supported | boolean | RFC 8707 support (true) |
| service_documentation | string | URL to service documentation |
Error Responses:
| Scenario | HTTP Status | Error |
|---|---|---|
| Wrong HTTP method | 400 | APP_REQUEST_TYPE |
GET /.well-known/oauth-protected-resource/
RFC 9728 Protected Resource Metadata. Returns resource server configuration.
/.well-known/oauth-protected-resource/
Auth: None · Rate Limit: 60/min, 600/hr per IP
curl -X GET "https://api.fast.io/.well-known/oauth-protected-resource/"
Response (200 OK):
{
"resource": "https://mcp.fast.io/mcp",
"authorization_servers": ["https://api.fast.io"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["user"]
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| resource | string | Protected resource identifier URL |
| authorization_servers | array | Authorization server issuer URLs that can issue tokens for this resource |
| bearer_methods_supported | array | Methods for presenting bearer tokens (header — Authorization header only) |
| scopes_supported | array | OAuth scopes supported by this resource |
Error Responses:
| Scenario | HTTP Status | Error |
|---|---|---|
| Wrong HTTP method | 400 | APP_REQUEST_TYPE |
Dynamic Client Registration
POST /current/oauth/register/
RFC 7591 Dynamic Client Registration. Register a new OAuth client programmatically.
/current/oauth/register/
Auth: None · Rate Limit: 5/min, 20/hr per IP · Content-Type: application/json or application/x-www-form-urlencoded
Request Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| client_name | string | No | "Unknown Client" | Human-readable client name (max 128 characters) |
| redirect_uris | JSON array | Yes | — | Allowed redirect URIs. 1–10 URIs. HTTPS required (localhost/127.0.0.1 exempt). No fragment (#) components. |
| token_endpoint_auth_method | string | No | "none" | Auth method (only public clients supported) |
| grant_types | JSON array | No | ["authorization_code", "refresh_token"] | Requested grant types |
| response_types | JSON array | No | ["code"] | Requested response types |
curl -X POST "https://api.fast.io/current/oauth/register/" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3000/callback", "http://127.0.0.1:3000/callback"]
}'
Response (200 OK):
{
"result": true,
"response": {
"client_id": "dyn_a1b2c3d4e5f6g7h8i9j0",
"client_name": "My MCP Client",
"redirect_uris": [
"http://localhost:3000/callback",
"http://127.0.0.1:3000/callback"
],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"registration_access_token": "rat_a1b2c3d4e5f6g7h8...",
"registration_client_uri": "https://api.fast.io/current/oauth/register/"
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.client_id | string | Assigned client identifier for OAuth flows |
| response.client_name | string | Registered client name |
| response.redirect_uris | array | Registered redirect URIs |
| response.token_endpoint_auth_method | string | Token endpoint auth method ("none") |
| response.grant_types | array | Granted grant types |
| response.response_types | array | Granted response types |
| response.registration_access_token | string | One-time token for managing registration via PUT. Shown once only — store securely. Server stores only a SHA-256 hash. |
| response.registration_client_uri | string | URI for managing this client registration |
Error Responses (RFC 7591 format — bare JSON, no platform envelope):
| Error Code | HTTP Status | Description |
|---|---|---|
invalid_client_metadata | 400 | client_name exceeds 128 characters |
invalid_client_metadata | 400 | redirect_uris is not a valid JSON array |
invalid_client_metadata | 400 | token_endpoint_auth_method is not "none" |
invalid_redirect_uri | 400 | redirect_uris must contain 1–10 entries |
invalid_redirect_uri | 400 | Redirect URI is not a valid URL |
invalid_redirect_uri | 400 | Redirect URI contains a fragment (#) |
invalid_redirect_uri | 400 | Redirect URI must use HTTPS (except localhost) |
invalid_request | 400 | redirect_uris is missing |
server_error | 500 | Failed to register client |
PUT /current/oauth/register/
RFC 7592 Dynamic Client Registration Management. Update an existing client registration. Requires the registration_access_token from the POST registration response. Only clients whose redirect URIs all point to localhost, 127.0.0.1, [::1], or ::1 may self-update.
/current/oauth/register/
Auth: Registration access token (Bearer) · Rate Limit: 5/min, 20/hr per IP · Content-Type: application/json or application/x-www-form-urlencoded
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_id | string | Yes | Must match a registered client |
| client_name | string | No | Updated client name (max 128 characters) |
| redirect_uris | JSON array | No | Updated redirect URIs (full replacement, same validation as POST) |
At least one of client_name or redirect_uris must be provided.
curl -X PUT "https://api.fast.io/current/oauth/register/" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer rat_a1b2c3d4e5f6g7h8..." \
-d '{
"client_id": "dyn_a1b2c3d4e5f6g7h8i9j0",
"client_name": "My Updated MCP Client",
"redirect_uris": ["http://localhost:8080/callback"]
}'
Response (200 OK):
{
"result": true,
"response": {
"client_id": "dyn_a1b2c3d4e5f6g7h8i9j0",
"client_name": "My Updated MCP Client",
"redirect_uris": ["http://localhost:8080/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.client_id | string | Client identifier (unchanged) |
| response.client_name | string | Updated client name |
| response.redirect_uris | array | Updated redirect URIs |
| response.token_endpoint_auth_method | string | Auth method (unchanged, always "none") |
| response.grant_types | array | Grant types (unchanged) |
| response.response_types | array | Response types (unchanged) |
Error Responses (RFC 7591 format — bare JSON):
| Error Code | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | client_id missing |
invalid_request | 400 | Neither client_name nor redirect_uris provided |
invalid_request | 400 | Only localhost clients can self-update |
invalid_request | 401 | Invalid or missing registration access token |
invalid_client | 404 | Client not found or disabled |
invalid_client_metadata | 400 | Invalid client metadata |
invalid_redirect_uri | 400 | Invalid redirect URIs (same rules as POST) |
server_error | 500 | Failed to update registration |
The
grant_types,response_types, andtoken_endpoint_auth_methodfields cannot be changed via self-update. Ifredirect_urisis provided, it fully replaces the existing set. If onlyclient_nameis provided, existing redirect URIs are preserved.
Client ID Metadata Document (CIMD)
CIMD allows OAuth clients to use an HTTPS URL as their client_id instead of pre-registering or using Dynamic Client Registration. The authorization server fetches metadata from the URL to get client information on-the-fly. This is the MCP specification's preferred client registration method.
How It Works
- Detection: If the
client_idparameter starts withhttps://, the server treats it as a CIMD URL. - Fetch: The server fetches the JSON metadata document from the CIMD URL.
- Validation: The document must contain:
client_idmatching the fetched URL exactlyredirect_urisarray containing the requested redirect URIgrant_typesincludingauthorization_coderesponse_typesincludingcodetoken_endpoint_auth_methodset tonone
- Caching: Validated documents are cached for 1 hour to avoid repeated fetches.
- Flow: The CIMD URL is used as the
client_idthroughout the authorization flow (authorize, token exchange, refresh).
CIMD Document Format
The metadata document is a JSON file served at an HTTPS URL with Content-Type: application/json:
{
"client_id": "https://example.com/oauth/client-metadata",
"client_name": "Example App",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Optional fields: client_uri (URL to the client's home page), logo_uri (URL to the client's logo image).
Security Constraints
| Constraint | Value |
|---|---|
| Protocol | HTTPS only (HTTP URLs rejected) |
| Fetch timeout | 5 seconds |
| Max document size | 10 KB |
| Cross-domain redirects | Rejected |
| Cache duration | 1 hour |
Using CIMD in the Authorization Flow
Use the CIMD URL directly as the client_id parameter:
GET /current/oauth/authorize/?client_id=https://example.com/oauth/client-metadata&redirect_uri=http://localhost:8080/callback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=xyz123
The token endpoint also uses the CIMD URL as client_id — it matches by string comparison against the value stored during authorization.
Error Scenarios
| Scenario | Error |
|---|---|
| CIMD URL is not HTTPS | APP_ERROR_INPUT_INVALID (400) |
| Cannot fetch the document (timeout, DNS failure, non-200 response) | APP_ERROR_INPUT_INVALID (400) |
| Document is not valid JSON | APP_ERROR_INPUT_INVALID (400) |
client_id in document does not match the URL | APP_ERROR_INPUT_INVALID (400) |
| Required fields missing or invalid | APP_ERROR_INPUT_INVALID (400) |
redirect_uri not found in document's redirect_uris | APP_ERROR_INPUT_INVALID (400) |
Complete PKCE Flow
Step 0: Discover Server Configuration (Optional)
Fetch server metadata for automated setup:
1. CLIENT -> API: Discover authorization server
GET /.well-known/oauth-authorization-server/
-> Returns endpoints, supported grant types, PKCE methods
2. CLIENT -> API: Discover protected resource (if needed)
GET /.well-known/oauth-protected-resource/
-> Returns resource URL and authorization servers
Step 0b: Register Client (If Needed)
If the client does not have a registered client_id, there are two options:
Option A: Use a CIMD URL as client_id — If the client publishes a metadata document at an HTTPS URL, use that URL directly as the client_id. No registration step needed.
Option B: Use Dynamic Client Registration:
CLIENT -> API: Register client
POST /current/oauth/register/
Content-Type: application/json
{"client_name": "My Agent", "redirect_uris": ["http://localhost:8080/callback"]}
-> Returns client_id, redirect_uris, registration_access_token, etc.
Step 1: Generate PKCE Parameters (Client-Side)
Before initiating the flow, generate the PKCE code verifier and challenge:
code_verifier = random_string(43-128 characters, URL-safe: [A-Za-z0-9-._~])
code_challenge = base64url_encode(sha256(code_verifier))
code_challenge_method = "S256"
The code_challenge will always be exactly 43 characters.
Step 2: Initiate Authorization
GET /current/oauth/authorize/
Initiates the authorization flow. By default, issues a 302 Found redirect to the login/consent page. This is the browser-facing entry point that MCP clients open in the user's browser.
/current/oauth/authorize/
Auth: None · Rate Limit: 30/min, 300/hr per IP
JSON mode: Add response_format=json to receive a JSON response instead of a redirect (for programmatic callers).
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| client_id | string | Yes | — | Registered OAuth client ID, or an HTTPS URL pointing to a CIMD |
| redirect_uri | string | Yes | — | Must match a registered URI for the client |
| response_type | string | Yes | — | Must be "code" |
| code_challenge | string | Yes | — | Exactly 43 characters (BASE64URL-encoded SHA-256) |
| code_challenge_method | string | Yes | — | Must be "S256" |
| state | string | Yes | — | Opaque value for CSRF protection, returned unchanged in callback |
| scope | string | No | "user" | Scope type selector (see scope values table) |
| resource | string | No | — | RFC 8707 resource indicator (e.g., https://mcp.fast.io/mcp) |
| agent_name | string | No | — | Display name of the requesting agent (max 128 characters) |
| response_format | string | No | — | Set to "json" for JSON response instead of 302 redirect |
Scope type values (scope parameter):
| Value | Behavior |
|---|---|
user | Full access (default, backward compatible with v1.0 JWT) |
org | User picks specific organizations |
workspace | User picks specific workspaces |
all_orgs | Wildcard access to all user's organizations |
all_workspaces | Wildcard access to all user's workspaces |
all_shares | All shares the user is a member of |
# Standard browser flow (302 redirect)
curl -v "https://api.fast.io/current/oauth/authorize/?client_id=my-app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&response_type=code&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=abc123xyz"
# JSON mode (programmatic)
curl "https://api.fast.io/current/oauth/authorize/?client_id=my-app&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&response_type=code&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=abc123xyz&response_format=json"
Response (default — 302 Found):
Location: https://go.fast.io/connect?auth_request_id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Response (response_format=json — 200 OK):
{
"result": true,
"response": {
"auth_request_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"client_name": "My App",
"scope": "user"
}
}
Response Fields (JSON mode):
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.auth_request_id | string | 64-character hex identifier for this authorization request (valid for 10 minutes) |
| response.client_name | string | Human-readable name of the OAuth client application |
| response.scope | string | The granted scope |
| response.agent_name | string | Agent display name (present if agent_name was provided) |
Error Responses (standard platform envelope):
| Scenario | Error Code | HTTP Status |
|---|---|---|
client_id missing | APP_ERROR_INPUT_INVALID | 400 |
redirect_uri missing | APP_ERROR_INPUT_INVALID | 400 |
code_challenge missing | APP_ERROR_INPUT_INVALID | 400 |
code_challenge_method missing | APP_ERROR_INPUT_INVALID | 400 |
code_challenge_method not "S256" | APP_ERROR_INPUT_INVALID | 400 |
code_challenge not 43 characters | APP_ERROR_INPUT_INVALID | 400 |
state missing | APP_ERROR_INPUT_INVALID | 400 |
client_id not found | APP_ERROR_INPUT_INVALID | 400 |
| Client is disabled | APP_ERROR_INPUT_INVALID | 400 |
redirect_uri mismatch | APP_ERROR_INPUT_INVALID | 400 |
Invalid resource indicator | APP_ERROR_INPUT_INVALID | 400 |
| Redis storage failure | APP_INTERNAL_ERROR | 500 |
POST /current/oauth/authorize/
Complete authorization after user login. Issues the authorization code. Called by the web application (not the client directly) with the user's active session.
/current/oauth/authorize/
Auth: Required (JWT — website session) · Rate Limit: 20/min, 200/hr per IP
Request Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| auth_request_id | string | Yes | — | 64-character hex authorization request ID from the GET request |
| scopes | string | No | — | JSON array of entity IDs or scope strings (e.g., "[12345, 67890]" or '["org:12345:rw"]') |
| access_mode | string | No | "rw" | "r" (read-only) or "rw" (read-write) |
| agent_name | string | No | — | Override the agent display name from the GET request (max 128 characters) |
curl -X POST "https://api.fast.io/current/oauth/authorize/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "auth_request_id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
Response (200 OK):
{
"result": true,
"response": {
"redirect_uri": "http://localhost:8080/callback?code=abc123def456abc123def456abc123def456abc123def456abc123def456abc123de&state=abc123xyz",
"redirect_mode": "redirect"
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.redirect_uri | string | Full redirect URI with code (64 hex chars, valid 5 min, single-use) and state query parameters |
| response.redirect_mode | string | "redirect" for HTTP/HTTPS redirect URIs, "button" for custom scheme URIs |
| response.button_label | string or null | Custom button label for "button" redirect mode (if configured) |
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| User not authenticated | APP_AUTH_INVALID | 401 |
auth_request_id missing | APP_ERROR_INPUT_INVALID | 400 |
| Request expired or not found | APP_ERROR_NOT_FOUND | 404 |
| Corrupted auth request data | APP_INTERNAL_ERROR | 500 |
Invalid access_mode | APP_ERROR_INPUT_INVALID | 400 |
| Scope validation failed | APP_ERROR_INPUT_INVALID | 400 |
| Failed to store auth code | APP_INTERNAL_ERROR | 500 |
GET /current/oauth/authorize/info/
Validate an authorization request and return client information (app name, requested scope). Used by the web frontend to display a consent screen before the user confirms.
/current/oauth/authorize/info/
Auth: None · Rate Limit: 60/min, 600/hr per IP
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| auth_request_id | string | Yes | The authorization request ID to validate |
curl "https://api.fast.io/current/oauth/authorize/info/?auth_request_id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
Response (valid request — 200 OK):
{
"result": true,
"response": {
"valid": true,
"client_name": "My App",
"scope": "user",
"agent_name": "My MCP Agent",
"redirect_mode": "redirect",
"redirect_uri": "http://localhost:8080/callback"
}
}
Response (invalid or expired — 200 OK):
{
"result": true,
"response": {
"valid": false
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | Always true |
| response.valid | boolean | true if the auth request is valid and client is active |
| response.client_name | string | Client name (only when valid is true) |
| response.scope | string | Requested scope (only when valid is true) |
| response.agent_name | string | Agent display name (only when set and valid is true) |
| response.redirect_mode | string | "redirect" or "button" (only when valid is true) |
| response.button_label | string | Custom button label (only when configured and valid is true) |
| response.redirect_uri | string | Client redirect URI (only when valid is true) |
This endpoint never returns an error for invalid
auth_request_id. It returnsvalid: falseinstead, preventing information leakage about authorization request existence.
Step 3: User Approves in Browser
The user opens the authorization URL, signs in (supports SSO), and approves access. The browser either:
- Redirects to
redirect_uriwith?code={authorization_code}&state={state} - Displays the authorization code for the user to copy back to the agent (when
redirect_modeis"button")
Step 4: Exchange Code for Tokens
POST /current/oauth/token/
Exchange an authorization code for access and refresh tokens, or refresh an existing access token.
/current/oauth/token/
Auth: None · Content-Type: application/x-www-form-urlencoded · Rate Limit: 20/min, 200/hr per IP
Important: This endpoint returns bare JSON responses (RFC 6749 format, no platform envelope) for compatibility with standard OAuth clients, including MCP hosts.
Authorization Code Exchange
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | Must be "authorization_code" |
| code | string | Yes | 64-character hex authorization code from the callback |
| code_verifier | string | Yes | Original PKCE code verifier (43–128 characters, [A-Za-z0-9-._~]) |
| client_id | string | Yes | Must match original request |
| redirect_uri | string | Yes | Must match original request |
| resource | string | No | RFC 8707 resource indicator URL (must match authorize request) |
| device_name | string | No | Human-readable device name for session tracking |
| device_type | string | No | Device category for session tracking |
curl -X POST "https://api.fast.io/current/oauth/token/" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=abc123def456abc123def456abc123def456abc123def456abc123def456abc123de&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&client_id=my-app&redirect_uri=http://localhost:8080/callback"
Response (200 OK — bare JSON, no envelope):
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "org",
"scopes": "[\"org:12345:rw\",\"org:67890:rw\"]",
"agent_name": "My MCP Agent"
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| access_token | string | JWT for API requests. Use as Authorization: Bearer {access_token}. Expires in 1 hour. |
| token_type | string | Always "Bearer" |
| expires_in | integer | Token lifetime in seconds (3600 = 1 hour) |
| refresh_token | string | Long-lived token for obtaining new access tokens. Valid for 30 days. Store securely — only returned once, stored as hash. |
| scope | string | Granted scope type |
| scopes | string | JSON-encoded array of scope strings in entity_type:entity_id:access_mode format (v2.0 tokens only, absent for user scope without explicit scopes) |
| agent_name | string | Agent display name (v2.0 only, present when set during authorization) |
Refresh Token Exchange
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | Must be "refresh_token" |
| refresh_token | string | Yes | Current valid refresh token |
| client_id | string | Yes | Must match the original token request |
| device_name | string | No | Updated device name for session tracking |
| device_type | string | No | Updated device type for session tracking |
curl -X POST "https://api.fast.io/current/oauth/token/" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token&refresh_token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...&client_id=my-app"
Response (200 OK — bare JSON): Same format as authorization code exchange. Returns a new access_token and a new refresh_token. The old refresh token is immediately revoked (mandatory token rotation).
Error Responses (RFC 6749 SS5.2 format — bare JSON):
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid or has expired."
}
| Scenario | RFC 6749 Error | HTTP Status |
|---|---|---|
| Missing/invalid parameters | invalid_request | 400 |
Invalid grant_type value | unsupported_grant_type | 400 |
code_verifier invalid length (not 43–128 chars) | invalid_request | 400 |
Unrecognized resource indicator | invalid_request | 400 |
| Invalid/expired authorization code | invalid_grant | 400 |
PKCE code_verifier mismatch | invalid_grant | 400 |
client_id mismatch | invalid_grant | 400 |
redirect_uri mismatch | invalid_grant | 400 |
| Resource indicator mismatch (RFC 8707) | invalid_grant | 400 |
| Invalid/expired/revoked refresh token | invalid_grant | 400 |
client_id mismatch on refresh | invalid_grant | 400 |
| Inactive user account | invalid_grant | 400 |
| Internal server failure | server_error | 500 |
Resource Indicator Enforcement: The
resourcevalue is stored in the authorization code during the authorize step. At token exchange, the value is strictly compared. If they do not match — including if one isnulland the other is not — the exchange fails. This prevents downgrade attacks where a client omits the resource to obtain an unrestricted token.
Step 5: Use the Access Token
Include the access token in all API requests:
Authorization: Bearer {access_token}
When the access token expires (after 1 hour), use the refresh token to get a new one without requiring user interaction. Refresh proactively before expiration (5-minute buffer recommended) to avoid interruptions.
Token Revocation
POST /current/oauth/revoke/
Revoke a refresh token (logout). Implements RFC 7009 (OAuth 2.0 Token Revocation). Always returns success to prevent token enumeration attacks.
/current/oauth/revoke/
Auth: None · Content-Type: application/x-www-form-urlencoded · Rate Limit: 30/min, 300/hr per IP
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | The refresh token to revoke |
curl -X POST "https://api.fast.io/current/oauth/revoke/" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
Response (200 OK):
{
"result": true,
"response": {
"result": true
}
}
Error Responses (RFC 6749 format — bare JSON):
| Scenario | Error | HTTP Status |
|---|---|---|
token parameter missing | invalid_request | 400 |
Per RFC 7009, this endpoint always returns success regardless of whether the token was found, was already revoked, or never existed. Always call this endpoint on user logout and clear local token storage regardless of the response.
Session Management
OAuth sessions represent active token grants. Each authorization code exchange creates a session. Sessions have a stable session_id (32-character hex string) that persists across token rotations.
GET /current/oauth/sessions/
List all active (non-expired, non-revoked) OAuth sessions for the authenticated user.
/current/oauth/sessions/
Auth: Required (Bearer JWT) · Rate Limit: 30/min, 300/hr per IP
curl -X GET "https://api.fast.io/current/oauth/sessions/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"response": {
"sessions": [
{
"session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"client_id": "my-app",
"device_name": "Chrome on macOS",
"device_type": "desktop",
"ip_address": "192.168.1.100",
"last_used": "2026-01-22 14:00:00",
"created": "2026-01-21 14:00:00",
"expires": "2026-02-21 14:00:00"
}
],
"count": 1
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.sessions | array | List of active session objects |
| response.sessions[].session_id | string | 32-character hex session identifier |
| response.sessions[].client_id | string | OAuth client that created the session |
| response.sessions[].device_name | string | Human-readable device description |
| response.sessions[].device_type | string | Device category: desktop, mobile, tablet, or unknown |
| response.sessions[].ip_address | string | IP address at session creation |
| response.sessions[].last_used | string | Datetime of last token refresh (YYYY-MM-DD HH:MM:SS) |
| response.sessions[].created | string | Datetime when the session was created |
| response.sessions[].expires | string | Datetime when the session expires |
| response.count | integer | Total number of active sessions |
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | APP_AUTH_INVALID | 401 |
| Database error | APP_INTERNAL_ERROR | 500 |
GET /current/oauth/sessions/{session_id}/
Get details of a specific OAuth session. The session must belong to the authenticated user.
/current/oauth/sessions/{session_id}/
Auth: Required (Bearer JWT) · Rate Limit: 30/min, 300/hr per IP
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| session_id | string | Yes | 32 hexadecimal characters |
curl -X GET "https://api.fast.io/current/oauth/sessions/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"response": {
"session": {
"session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"client_id": "my-app",
"device_name": "Chrome on macOS",
"device_type": "desktop",
"ip_address": "192.168.1.100",
"last_used": "2026-01-22 14:00:00",
"created": "2026-01-21 14:00:00",
"expires": "2026-02-21 14:00:00"
}
}
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | APP_AUTH_INVALID | 401 |
session_id missing | APP_ERROR_INPUT_INVALID | 400 |
session_id not 32 hex chars | APP_ERROR_INPUT_INVALID | 400 |
| Session not found or wrong user | APP_ERROR_NOT_FOUND | 404 |
| Database error | APP_INTERNAL_ERROR | 500 |
PATCH /current/oauth/sessions/{session_id}/
Update the device_name and/or agent_name of a specific OAuth session. The session must belong to the authenticated user and must not be revoked.
/current/oauth/sessions/{session_id}/
Auth: Required (Bearer JWT) · Rate Limit: 20/min, 100/hr per IP
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| session_id | string | Yes | 32 hexadecimal characters |
Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| device_name | string | No | New device name (max 128 characters; empty string clears to null) |
| agent_name | string | No | New agent name (max 128 characters; empty string clears to null) |
At least one of device_name or agent_name must be provided.
curl -X PATCH "https://api.fast.io/current/oauth/sessions/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "device_name=My%20Work%20Laptop"
Response (200 OK):
{
"result": true,
"response": {
"session": {
"session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"client_id": "my-app",
"device_name": "My Work Laptop",
"device_type": "desktop",
"ip_address": "192.168.1.100",
"last_used": "2026-01-22 14:00:00",
"created": "2026-01-21 14:00:00",
"expires": "2026-02-21 14:00:00"
}
}
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | APP_AUTH_INVALID | 401 |
session_id missing | APP_ERROR_INPUT_INVALID | 400 |
session_id not 32 hex chars | APP_ERROR_INPUT_INVALID | 400 |
| Neither parameter provided | APP_ERROR_INPUT_INVALID | 400 |
device_name exceeds 128 chars | APP_ERROR_INPUT_INVALID | 400 |
agent_name exceeds 128 chars | APP_ERROR_INPUT_INVALID | 400 |
| Session is revoked | APP_ERROR_INPUT_INVALID | 400 |
| Session not found or wrong user | APP_ERROR_NOT_FOUND | 404 |
| Database error | APP_INTERNAL_ERROR | 500 |
DELETE /current/oauth/sessions/{session_id}/
Revoke a specific OAuth session. The session must belong to the authenticated user. This operation is idempotent — if the session is already revoked, success is still returned.
/current/oauth/sessions/{session_id}/
Auth: Required (Bearer JWT) · Rate Limit: 20/min, 100/hr per IP
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| session_id | string | Yes | 32 hexadecimal characters |
curl -X DELETE "https://api.fast.io/current/oauth/sessions/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"response": {
"result": true,
"message": "Session has been revoked."
}
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | APP_AUTH_INVALID | 401 |
session_id missing | APP_ERROR_INPUT_INVALID | 400 |
session_id not 32 hex chars | APP_ERROR_INPUT_INVALID | 400 |
| Session not found or wrong user | APP_ERROR_NOT_FOUND | 404 |
| Database error | APP_INTERNAL_ERROR | 500 |
DELETE /current/oauth/sessions/
Revoke all OAuth sessions (logout everywhere). Optionally exclude the current session for "log out everywhere else" functionality.
/current/oauth/sessions/
Auth: Required (Bearer JWT) · Rate Limit: 10/min, 50/hr per IP
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| exclude_current | string | No | — | Set to "true" or "1" to keep the current session active |
| current_session_id | string | No | — | Session ID to preserve (required when exclude_current is set) |
# Revoke ALL sessions
curl -X DELETE "https://api.fast.io/current/oauth/sessions/" \
-H "Authorization: Bearer {jwt_token}"
# Revoke all EXCEPT current session
curl -X DELETE "https://api.fast.io/current/oauth/sessions/?exclude_current=true¤t_session_id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"response": {
"result": true,
"message": "All sessions have been revoked."
}
}
Or when exclude_current is used:
{
"result": true,
"response": {
"result": true,
"message": "All other sessions have been revoked."
}
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | APP_AUTH_INVALID | 401 |
| Database error | APP_INTERNAL_ERROR | 500 |
When
exclude_currentis"true"butcurrent_session_idis not provided, all sessions are revoked.
Token Scope Introspection
GET /current/auth/scopes/
Returns the current token's scope information, auth type, and agent status. Enables clients to discover their token's capabilities without decoding the JWT.
/current/auth/scopes/
Auth: Required (Bearer JWT or API Key) · Rate Limit: 60/min, 600/hr per user + IP
curl -X GET "https://api.fast.io/current/auth/scopes/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"response": {
"auth_type": "jwt_v2",
"scopes": ["org:12345:rw", "org:67890:rw"],
"scopes_detail": [
{
"entity_type": "org",
"entity_id": "12345",
"access_mode": "rw",
"name": "Acme Corp",
"domain": "acme"
},
{
"entity_type": "org",
"entity_id": "67890",
"access_mode": "rw",
"name": "Beta Inc",
"domain": "beta"
}
],
"is_agent": true,
"agent_name": "My MCP Agent",
"full_access": false
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.auth_type | string | "jwt_v2" (scoped JWT), "jwt_v1" (legacy JWT), or "api_key" |
| response.scopes | array | Scope strings in entity_type:entity_id:access_mode format. Empty for v1.0 JWT and API key. |
| response.scopes_detail | array | Hydrated scope objects with entity names and metadata. Empty for v1.0 JWT and API key. |
| response.is_agent | boolean | Whether the token represents an agent |
| response.agent_name | string or null | Agent display name if set, otherwise null |
| response.full_access | boolean | true for v1.0 JWT, API keys, and v2.0 tokens with user:*:rw scope |
Scope Detail Fields:
Each entry in scopes_detail contains entity-specific fields:
| Field | Type | Present For | Description |
|---|---|---|---|
| entity_type | string | All | user, org, workspace, or share |
| entity_id | string | All | Numeric ID or * for wildcard |
| access_mode | string | All | r (read) or rw (read/write) |
| label | string | Full access / wildcard | Human-readable label (e.g., "Full Access", "All Organizations") |
| name | string | Org, Workspace | Entity display name |
| domain | string | Org | Organization subdomain |
| folder_name | string | Workspace | Workspace URL slug |
| org_id | integer | Workspace, Share | Parent organization ID |
| org_name | string | Workspace, Share | Parent organization name |
| org_domain | string | Workspace, Share | Parent organization subdomain |
| title | string | Share | Share display title |
| share_type | string | Share | send, receive, or exchange |
| workspace_id | integer | Share | Parent workspace ID |
| workspace_name | string | Share | Parent workspace name |
| load_error | string | On failure | "Entity not found" if the referenced entity could not be loaded |
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | APP_AUTH_INVALID | 401 |
| Wrong HTTP method | APP_REQUEST_TYPE | 400 |
Complete PKCE Example Flow
Here is a complete example of the PKCE authorization flow:
0. CLIENT -> API: Discover server configuration (optional)
GET /.well-known/oauth-authorization-server/
-> Returns endpoints, grant types, PKCE methods, resource_indicators_supported
0b. CLIENT -> API: Register client dynamically (if no client_id)
POST /current/oauth/register/
Content-Type: application/json
{"client_name": "My Agent", "redirect_uris": ["http://localhost:8080/callback"]}
-> Returns client_id, registration_access_token
OR use a CIMD URL as client_id (no registration needed)
1. CLIENT: Generate PKCE parameters
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = base64url(sha256(code_verifier))
= "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
2. CLIENT -> BROWSER: Open authorization URL in user's browser
GET /current/oauth/authorize/
?client_id=my-app
&redirect_uri=http://localhost:8080/callback
&response_type=code
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=xyz123
&resource=https://mcp.fast.io/mcp
3. API -> BROWSER: 302 redirect to login/consent page
-> Browser lands on login page, user signs in, approves access
4. BROWSER -> CLIENT: Authorization code returned
http://localhost:8080/callback?code=AUTH_CODE_HERE&state=xyz123
(or displayed on screen for user to copy)
5. CLIENT -> API: Exchange code for tokens
POST /current/oauth/token/
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_HERE
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=my-app
&redirect_uri=http://localhost:8080/callback
&resource=https://mcp.fast.io/mcp
6. API -> CLIENT: Tokens returned (bare JSON)
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJ...",
"scope": "user"
}
7. CLIENT -> API: Use access token for requests
GET /current/user/me/details/
Authorization: Bearer eyJ...
8. CLIENT -> API: Refresh when access token expires
POST /current/oauth/token/
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=eyJ...
&client_id=my-app
-> Returns new access_token + new refresh_token (old one revoked)
9. CLIENT -> API: Revoke on logout
POST /current/oauth/revoke/
Content-Type: application/x-www-form-urlencoded
token=eyJ...
Error Handling
Token and Revoke Endpoints (RFC 6749 SS5.2)
The token (POST /current/oauth/token/) and revoke (POST /current/oauth/revoke/) endpoints return RFC 6749 compliant error responses — bare JSON, not the standard platform envelope:
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid or has expired."
}
Registration Endpoint (RFC 7591)
The registration endpoint (POST /current/oauth/register/ and PUT /current/oauth/register/) also returns bare JSON error responses with RFC 7591 error codes:
{
"error": "invalid_client_metadata",
"error_description": "The client_name must be between 1 and 128 characters."
}
Other OAuth Endpoints
All other OAuth endpoints (authorize, authorize/info, sessions) use the standard platform error envelope:
{
"result": false,
"error": {
"code": 1605,
"text": "Error message",
"documentation_url": "https://api.fast.io/llms.txt",
"resource": "GET /current/oauth/authorize/"
}
}
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Rate limited | APP_ENHANCE_CALM (1671) | 429 |