OAuth 2.0 PKCE flow, token exchange, session management, DCR, resource indicators

Base URL: https://api.fast.io/current/ Request format: application/x-www-form-urlencoded Response format: JSON

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:

Endpoint Summary

MethodPathAuthDescription
GET/.well-known/oauth-authorization-server/NoneRFC 8414 authorization server metadata
GET/.well-known/oauth-protected-resource/NoneRFC 9728 protected resource metadata
POST/current/oauth/register/NoneRFC 7591 dynamic client registration
PUT/current/oauth/register/Registration access token (Bearer)RFC 7592 update client registration
GET/current/oauth/authorize/NoneInitiate 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/NoneGet client info for consent screen
POST/current/oauth/token/NoneExchange code for tokens, or refresh tokens
POST/current/oauth/revoke/NoneRevoke a refresh token
GET/current/oauth/sessions/BearerList all active sessions
GET/current/oauth/sessions/{id}/BearerGet session details
PATCH/current/oauth/sessions/{id}/BearerUpdate session display names
DELETE/current/oauth/sessions/{id}/BearerRevoke a specific session
DELETE/current/oauth/sessions/BearerRevoke all sessions
GET/current/auth/scopes/BearerToken scope introspection

Metadata Discovery

GET /.well-known/oauth-authorization-server/

RFC 8414 Authorization Server Metadata. Returns server configuration for automated client setup.

GET /.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:

FieldTypeDescription
issuerstringAuthorization server issuer identifier URL
authorization_endpointstringURL of the authorization endpoint
token_endpointstringURL of the token endpoint
revocation_endpointstringURL of the token revocation endpoint
registration_endpointstringURL of the dynamic client registration endpoint
response_types_supportedarraySupported response types (code only)
grant_types_supportedarraySupported grant types (authorization_code, refresh_token)
token_endpoint_auth_methods_supportedarraySupported auth methods (none — public clients only)
code_challenge_methods_supportedarraySupported PKCE methods (S256 only)
resource_indicators_supportedbooleanRFC 8707 support (true)
service_documentationstringURL to service documentation

Error Responses:

ScenarioHTTP StatusError
Wrong HTTP method400APP_REQUEST_TYPE

GET /.well-known/oauth-protected-resource/

RFC 9728 Protected Resource Metadata. Returns resource server configuration.

GET /.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:

FieldTypeDescription
resourcestringProtected resource identifier URL
authorization_serversarrayAuthorization server issuer URLs that can issue tokens for this resource
bearer_methods_supportedarrayMethods for presenting bearer tokens (header — Authorization header only)
scopes_supportedarrayOAuth scopes supported by this resource

Error Responses:

ScenarioHTTP StatusError
Wrong HTTP method400APP_REQUEST_TYPE

Dynamic Client Registration

POST /current/oauth/register/

RFC 7591 Dynamic Client Registration. Register a new OAuth client programmatically.

POST /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:

ParameterTypeRequiredDefaultDescription
client_namestringNo"Unknown Client"Human-readable client name (max 128 characters)
redirect_urisJSON arrayYesAllowed redirect URIs. 1–10 URIs. HTTPS required (localhost/127.0.0.1 exempt). No fragment (#) components.
token_endpoint_auth_methodstringNo"none"Auth method (only public clients supported)
grant_typesJSON arrayNo["authorization_code", "refresh_token"]Requested grant types
response_typesJSON arrayNo["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:

FieldTypeDescription
resultbooleantrue on success
response.client_idstringAssigned client identifier for OAuth flows
response.client_namestringRegistered client name
response.redirect_urisarrayRegistered redirect URIs
response.token_endpoint_auth_methodstringToken endpoint auth method ("none")
response.grant_typesarrayGranted grant types
response.response_typesarrayGranted response types
response.registration_access_tokenstringOne-time token for managing registration via PUT. Shown once only — store securely. Server stores only a SHA-256 hash.
response.registration_client_uristringURI for managing this client registration

Error Responses (RFC 7591 format — bare JSON, no platform envelope):

Error CodeHTTP StatusDescription
invalid_client_metadata400client_name exceeds 128 characters
invalid_client_metadata400redirect_uris is not a valid JSON array
invalid_client_metadata400token_endpoint_auth_method is not "none"
invalid_redirect_uri400redirect_uris must contain 1–10 entries
invalid_redirect_uri400Redirect URI is not a valid URL
invalid_redirect_uri400Redirect URI contains a fragment (#)
invalid_redirect_uri400Redirect URI must use HTTPS (except localhost)
invalid_request400redirect_uris is missing
server_error500Failed 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.

PUT /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:

ParameterTypeRequiredDescription
client_idstringYesMust match a registered client
client_namestringNoUpdated client name (max 128 characters)
redirect_urisJSON arrayNoUpdated 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:

FieldTypeDescription
resultbooleantrue on success
response.client_idstringClient identifier (unchanged)
response.client_namestringUpdated client name
response.redirect_urisarrayUpdated redirect URIs
response.token_endpoint_auth_methodstringAuth method (unchanged, always "none")
response.grant_typesarrayGrant types (unchanged)
response.response_typesarrayResponse types (unchanged)

Error Responses (RFC 7591 format — bare JSON):

Error CodeHTTP StatusDescription
invalid_request400client_id missing
invalid_request400Neither client_name nor redirect_uris provided
invalid_request400Only localhost clients can self-update
invalid_request401Invalid or missing registration access token
invalid_client404Client not found or disabled
invalid_client_metadata400Invalid client metadata
invalid_redirect_uri400Invalid redirect URIs (same rules as POST)
server_error500Failed to update registration

The grant_types, response_types, and token_endpoint_auth_method fields cannot be changed via self-update. If redirect_uris is provided, it fully replaces the existing set. If only client_name is 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

  1. Detection: If the client_id parameter starts with https://, the server treats it as a CIMD URL.
  2. Fetch: The server fetches the JSON metadata document from the CIMD URL.
  3. Validation: The document must contain:
    • client_id matching the fetched URL exactly
    • redirect_uris array containing the requested redirect URI
    • grant_types including authorization_code
    • response_types including code
    • token_endpoint_auth_method set to none
  4. Caching: Validated documents are cached for 1 hour to avoid repeated fetches.
  5. Flow: The CIMD URL is used as the client_id throughout 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

ConstraintValue
ProtocolHTTPS only (HTTP URLs rejected)
Fetch timeout5 seconds
Max document size10 KB
Cross-domain redirectsRejected
Cache duration1 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

ScenarioError
CIMD URL is not HTTPSAPP_ERROR_INPUT_INVALID (400)
Cannot fetch the document (timeout, DNS failure, non-200 response)APP_ERROR_INPUT_INVALID (400)
Document is not valid JSONAPP_ERROR_INPUT_INVALID (400)
client_id in document does not match the URLAPP_ERROR_INPUT_INVALID (400)
Required fields missing or invalidAPP_ERROR_INPUT_INVALID (400)
redirect_uri not found in document's redirect_urisAPP_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.

GET /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:

ParameterTypeRequiredDefaultDescription
client_idstringYesRegistered OAuth client ID, or an HTTPS URL pointing to a CIMD
redirect_uristringYesMust match a registered URI for the client
response_typestringYesMust be "code"
code_challengestringYesExactly 43 characters (BASE64URL-encoded SHA-256)
code_challenge_methodstringYesMust be "S256"
statestringYesOpaque value for CSRF protection, returned unchanged in callback
scopestringNo"user"Scope type selector (see scope values table)
resourcestringNoRFC 8707 resource indicator (e.g., https://mcp.fast.io/mcp)
agent_namestringNoDisplay name of the requesting agent (max 128 characters)
response_formatstringNoSet to "json" for JSON response instead of 302 redirect

Scope type values (scope parameter):

ValueBehavior
userFull access (default, backward compatible with v1.0 JWT)
orgUser picks specific organizations
workspaceUser picks specific workspaces
all_orgsWildcard access to all user's organizations
all_workspacesWildcard access to all user's workspaces
all_sharesAll 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):

FieldTypeDescription
resultbooleantrue on success
response.auth_request_idstring64-character hex identifier for this authorization request (valid for 10 minutes)
response.client_namestringHuman-readable name of the OAuth client application
response.scopestringThe granted scope
response.agent_namestringAgent display name (present if agent_name was provided)

Error Responses (standard platform envelope):

ScenarioError CodeHTTP Status
client_id missingAPP_ERROR_INPUT_INVALID400
redirect_uri missingAPP_ERROR_INPUT_INVALID400
code_challenge missingAPP_ERROR_INPUT_INVALID400
code_challenge_method missingAPP_ERROR_INPUT_INVALID400
code_challenge_method not "S256"APP_ERROR_INPUT_INVALID400
code_challenge not 43 charactersAPP_ERROR_INPUT_INVALID400
state missingAPP_ERROR_INPUT_INVALID400
client_id not foundAPP_ERROR_INPUT_INVALID400
Client is disabledAPP_ERROR_INPUT_INVALID400
redirect_uri mismatchAPP_ERROR_INPUT_INVALID400
Invalid resource indicatorAPP_ERROR_INPUT_INVALID400
Redis storage failureAPP_INTERNAL_ERROR500

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.

POST /current/oauth/authorize/

Auth: Required (JWT — website session) · Rate Limit: 20/min, 200/hr per IP

Request Parameters:

ParameterTypeRequiredDefaultDescription
auth_request_idstringYes64-character hex authorization request ID from the GET request
scopesstringNoJSON array of entity IDs or scope strings (e.g., "[12345, 67890]" or '["org:12345:rw"]')
access_modestringNo"rw""r" (read-only) or "rw" (read-write)
agent_namestringNoOverride 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:

FieldTypeDescription
resultbooleantrue on success
response.redirect_uristringFull redirect URI with code (64 hex chars, valid 5 min, single-use) and state query parameters
response.redirect_modestring"redirect" for HTTP/HTTPS redirect URIs, "button" for custom scheme URIs
response.button_labelstring or nullCustom button label for "button" redirect mode (if configured)

Error Responses:

ScenarioError CodeHTTP Status
User not authenticatedAPP_AUTH_INVALID401
auth_request_id missingAPP_ERROR_INPUT_INVALID400
Request expired or not foundAPP_ERROR_NOT_FOUND404
Corrupted auth request dataAPP_INTERNAL_ERROR500
Invalid access_modeAPP_ERROR_INPUT_INVALID400
Scope validation failedAPP_ERROR_INPUT_INVALID400
Failed to store auth codeAPP_INTERNAL_ERROR500

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.

GET /current/oauth/authorize/info/

Auth: None · Rate Limit: 60/min, 600/hr per IP

Query Parameters:

ParameterTypeRequiredDescription
auth_request_idstringYesThe 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:

FieldTypeDescription
resultbooleanAlways true
response.validbooleantrue if the auth request is valid and client is active
response.client_namestringClient name (only when valid is true)
response.scopestringRequested scope (only when valid is true)
response.agent_namestringAgent display name (only when set and valid is true)
response.redirect_modestring"redirect" or "button" (only when valid is true)
response.button_labelstringCustom button label (only when configured and valid is true)
response.redirect_uristringClient redirect URI (only when valid is true)

This endpoint never returns an error for invalid auth_request_id. It returns valid: false instead, 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:

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.

POST /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:

ParameterTypeRequiredDescription
grant_typestringYesMust be "authorization_code"
codestringYes64-character hex authorization code from the callback
code_verifierstringYesOriginal PKCE code verifier (43–128 characters, [A-Za-z0-9-._~])
client_idstringYesMust match original request
redirect_uristringYesMust match original request
resourcestringNoRFC 8707 resource indicator URL (must match authorize request)
device_namestringNoHuman-readable device name for session tracking
device_typestringNoDevice 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:

FieldTypeDescription
access_tokenstringJWT for API requests. Use as Authorization: Bearer {access_token}. Expires in 1 hour.
token_typestringAlways "Bearer"
expires_inintegerToken lifetime in seconds (3600 = 1 hour)
refresh_tokenstringLong-lived token for obtaining new access tokens. Valid for 30 days. Store securely — only returned once, stored as hash.
scopestringGranted scope type
scopesstringJSON-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_namestringAgent display name (v2.0 only, present when set during authorization)

Refresh Token Exchange

ParameterTypeRequiredDescription
grant_typestringYesMust be "refresh_token"
refresh_tokenstringYesCurrent valid refresh token
client_idstringYesMust match the original token request
device_namestringNoUpdated device name for session tracking
device_typestringNoUpdated 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."
}
ScenarioRFC 6749 ErrorHTTP Status
Missing/invalid parametersinvalid_request400
Invalid grant_type valueunsupported_grant_type400
code_verifier invalid length (not 43–128 chars)invalid_request400
Unrecognized resource indicatorinvalid_request400
Invalid/expired authorization codeinvalid_grant400
PKCE code_verifier mismatchinvalid_grant400
client_id mismatchinvalid_grant400
redirect_uri mismatchinvalid_grant400
Resource indicator mismatch (RFC 8707)invalid_grant400
Invalid/expired/revoked refresh tokeninvalid_grant400
client_id mismatch on refreshinvalid_grant400
Inactive user accountinvalid_grant400
Internal server failureserver_error500

Resource Indicator Enforcement: The resource value 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 is null and 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.

POST /current/oauth/revoke/

Auth: None · Content-Type: application/x-www-form-urlencoded · Rate Limit: 30/min, 300/hr per IP

Request Parameters:

ParameterTypeRequiredDescription
tokenstringYesThe 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):

ScenarioErrorHTTP Status
token parameter missinginvalid_request400

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.

GET /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:

FieldTypeDescription
resultbooleantrue on success
response.sessionsarrayList of active session objects
response.sessions[].session_idstring32-character hex session identifier
response.sessions[].client_idstringOAuth client that created the session
response.sessions[].device_namestringHuman-readable device description
response.sessions[].device_typestringDevice category: desktop, mobile, tablet, or unknown
response.sessions[].ip_addressstringIP address at session creation
response.sessions[].last_usedstringDatetime of last token refresh (YYYY-MM-DD HH:MM:SS)
response.sessions[].createdstringDatetime when the session was created
response.sessions[].expiresstringDatetime when the session expires
response.countintegerTotal number of active sessions

Error Responses:

ScenarioError CodeHTTP Status
Not authenticatedAPP_AUTH_INVALID401
Database errorAPP_INTERNAL_ERROR500

GET /current/oauth/sessions/{session_id}/

Get details of a specific OAuth session. The session must belong to the authenticated user.

GET /current/oauth/sessions/{session_id}/

Auth: Required (Bearer JWT) · Rate Limit: 30/min, 300/hr per IP

Path Parameters:

ParameterTypeRequiredDescription
session_idstringYes32 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:

ScenarioError CodeHTTP Status
Not authenticatedAPP_AUTH_INVALID401
session_id missingAPP_ERROR_INPUT_INVALID400
session_id not 32 hex charsAPP_ERROR_INPUT_INVALID400
Session not found or wrong userAPP_ERROR_NOT_FOUND404
Database errorAPP_INTERNAL_ERROR500

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.

PATCH /current/oauth/sessions/{session_id}/

Auth: Required (Bearer JWT) · Rate Limit: 20/min, 100/hr per IP

Path Parameters:

ParameterTypeRequiredDescription
session_idstringYes32 hexadecimal characters

Body Parameters:

ParameterTypeRequiredDescription
device_namestringNoNew device name (max 128 characters; empty string clears to null)
agent_namestringNoNew 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:

ScenarioError CodeHTTP Status
Not authenticatedAPP_AUTH_INVALID401
session_id missingAPP_ERROR_INPUT_INVALID400
session_id not 32 hex charsAPP_ERROR_INPUT_INVALID400
Neither parameter providedAPP_ERROR_INPUT_INVALID400
device_name exceeds 128 charsAPP_ERROR_INPUT_INVALID400
agent_name exceeds 128 charsAPP_ERROR_INPUT_INVALID400
Session is revokedAPP_ERROR_INPUT_INVALID400
Session not found or wrong userAPP_ERROR_NOT_FOUND404
Database errorAPP_INTERNAL_ERROR500

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.

DELETE /current/oauth/sessions/{session_id}/

Auth: Required (Bearer JWT) · Rate Limit: 20/min, 100/hr per IP

Path Parameters:

ParameterTypeRequiredDescription
session_idstringYes32 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:

ScenarioError CodeHTTP Status
Not authenticatedAPP_AUTH_INVALID401
session_id missingAPP_ERROR_INPUT_INVALID400
session_id not 32 hex charsAPP_ERROR_INPUT_INVALID400
Session not found or wrong userAPP_ERROR_NOT_FOUND404
Database errorAPP_INTERNAL_ERROR500

DELETE /current/oauth/sessions/

Revoke all OAuth sessions (logout everywhere). Optionally exclude the current session for "log out everywhere else" functionality.

DELETE /current/oauth/sessions/

Auth: Required (Bearer JWT) · Rate Limit: 10/min, 50/hr per IP

Query Parameters:

ParameterTypeRequiredDefaultDescription
exclude_currentstringNoSet to "true" or "1" to keep the current session active
current_session_idstringNoSession 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&current_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:

ScenarioError CodeHTTP Status
Not authenticatedAPP_AUTH_INVALID401
Database errorAPP_INTERNAL_ERROR500

When exclude_current is "true" but current_session_id is 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.

GET /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:

FieldTypeDescription
resultbooleantrue on success
response.auth_typestring"jwt_v2" (scoped JWT), "jwt_v1" (legacy JWT), or "api_key"
response.scopesarrayScope strings in entity_type:entity_id:access_mode format. Empty for v1.0 JWT and API key.
response.scopes_detailarrayHydrated scope objects with entity names and metadata. Empty for v1.0 JWT and API key.
response.is_agentbooleanWhether the token represents an agent
response.agent_namestring or nullAgent display name if set, otherwise null
response.full_accessbooleantrue 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:

FieldTypePresent ForDescription
entity_typestringAlluser, org, workspace, or share
entity_idstringAllNumeric ID or * for wildcard
access_modestringAllr (read) or rw (read/write)
labelstringFull access / wildcardHuman-readable label (e.g., "Full Access", "All Organizations")
namestringOrg, WorkspaceEntity display name
domainstringOrgOrganization subdomain
folder_namestringWorkspaceWorkspace URL slug
org_idintegerWorkspace, ShareParent organization ID
org_namestringWorkspace, ShareParent organization name
org_domainstringWorkspace, ShareParent organization subdomain
titlestringShareShare display title
share_typestringSharesend, receive, or exchange
workspace_idintegerShareParent workspace ID
workspace_namestringShareParent workspace name
load_errorstringOn failure"Entity not found" if the referenced entity could not be loaded

Error Responses:

ScenarioError CodeHTTP Status
Not authenticatedAPP_AUTH_INVALID401
Wrong HTTP methodAPP_REQUEST_TYPE400

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/"
  }
}
ScenarioError CodeHTTP Status
Rate limitedAPP_ENHANCE_CALM (1671)429
↑ Back to top