OAuth 2.0 PKCE flow, token exchange, session management, DCR, resource indicators
Overview
Fastio 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 are long-lived
- 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, or narrow its scopes |
| 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
curl -X GET "https://api.fast.io/.well-known/oauth-authorization-server/"
Response (200 OK):
{
"issuer": "https://go.fast.io",
"authorization_endpoint": "https://go.fast.io/api/current/oauth/authorize/",
"token_endpoint": "https://go.fast.io/api/current/oauth/token/",
"revocation_endpoint": "https://go.fast.io/api/current/oauth/revoke/",
"registration_endpoint": "https://go.fast.io/api/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,
"scopes_supported": ["user", "org", "workspace", "all_orgs", "all_workspaces", "all_shares", "all_sign_envelopes"],
"access_modes_supported": ["r", "rw", "rwa"],
"service_documentation": "https://go.fast.io/api/current/llms/"
}
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) |
| scopes_supported | array | OAuth scope type selectors supported by the authorization server (see the scope-type values table under GET /current/oauth/authorize/) |
| access_modes_supported | array | Access modes the authorization server accepts on access_mode: ["r","rw","rwa"] |
| service_documentation | string | URL to service documentation |
Clients validating access_modes_supported against {r, rw} must accept rwa. The list now carries a third mode — read, write and administer — so a client that rejects unknown values will fail against it. scopes_supported is unchanged (one of its values, all_sign_envelopes, is retired and must not be requested — see Retired scopes below): userdetails is an entity type inside a credential's scope list, never a scope type, and is never advertised here.
Error Responses:
Reading the error tables: the four-digit
16xx/17xxvalues below are HTTP-status classes, noterror.code. Theerror.codea client actually receives is assigned per endpoint, so use the HTTP status as the gate and a documentederror.code— five or six digits, plus the9661-9669family — only as a refinement. A16xxvalue identifies the status class — useful for telling which kind of failure occurred — but comparing one againsterror.codewill never match. Five- and six-digit codes (and the9661-9669family) are realerror.codevalues. If you widen a check from a specific code to a status, widen what you assert with it — a status covers failures the narrower code did not, so a message written for that one code becomes a confident falsehood on the rest.
| Scenario | HTTP Status | Error |
|---|---|---|
| Wrong HTTP method | 405 | 10003 |
GET /.well-known/oauth-protected-resource/
RFC 9728 Protected Resource Metadata. Returns resource server configuration.
/.well-known/oauth-protected-resource/
Auth: None
curl -X GET "https://api.fast.io/.well-known/oauth-protected-resource/"
Response (200 OK):
{
"resource": "https://mcp.fast.io/mcp",
"authorization_servers": ["https://go.fast.io"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["user", "org", "workspace", "all_orgs", "all_workspaces", "all_shares", "all_sign_envelopes"]
}
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 | 405 | 10003 |
Dynamic Client Registration
POST /current/oauth/register/
RFC 7591 Dynamic Client Registration. Register a new OAuth client programmatically.
/current/oauth/register/
Auth: None · 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,
"client_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"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": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"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 (32-character lowercase-hex string, no prefix) |
| 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 (64-character lowercase-hex string, no prefix). 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) · 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 a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
-d '{
"client_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"client_name": "My Updated MCP Client",
"redirect_uris": ["http://localhost:8080/callback"]
}'
Response (200 OK):
{
"result": true,
"client_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"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 | 1605 (Invalid Input) |
| Cannot fetch the document (timeout, DNS failure, non-200 response) | 1605 (Invalid Input) |
| Document is not valid JSON | 1605 (Invalid Input) |
client_id in document does not match the URL | 1605 (Invalid Input) |
| Required fields missing or invalid | 1605 (Invalid Input) |
redirect_uri not found in document's redirect_uris | 1605 (Invalid Input) |
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
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) |
| access_mode | string | No | — | "r", "rw" or "rwa". The ceiling for the whole authorization — consent may narrow it, never widen it. An empty value is treated as omitted; any other non-empty value returns 406 180242 |
| account_settings | string | No | "0" | "1" to allow account-settings access (userdetails:*:rw) to be granted at consent, "0", an empty value, or omission for off. Any other value — including "true" and "false" — returns 406 117053 |
| 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 |
These six are the complete offered set. Request exactly one of these named values, or omit scope for the default user; a scope outside this set is not a valid access request — the one exception is the retired all_sign_envelopes, which is still accepted and follows wildcard defaulting for compatibility but must not be requested (see Retired scopes below).
access_mode and account_settings are ceilings recorded with the authorization request. They are set here, on the initiate request, and the consent step (POST /current/oauth/authorize/) can only narrow them. access_mode bounds the access mode of every granted scope; account_settings decides whether account-settings access may be granted at all. A client that never sends them cannot obtain administration (rwa) or account settings through OAuth — administration is opt-in on a request the client made explicitly, and the consent screen never offers it unprompted.
Retired scopes — all_workflows and all_sign_envelopes: no longer offered. The Workflows feature has been removed, so all_workflows is deliberately omitted from scopes_supported and must not be requested. The authorization server retains it only as a fail-closed tombstone, so a pre-existing token that still carries it resolves to nothing rather than erroring — it is not a capability you can request. The E-Signature feature has likewise been removed, and although all_sign_envelopes still appears in scopes_supported for compatibility, it must not be requested; a credential that still holds it reports a sign_envelope entity type, which no longer maps to any offered surface.
# 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,
"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 | 1605 (Invalid Input) | 406 |
redirect_uri missing | 1605 (Invalid Input) | 406 |
code_challenge missing | 1605 (Invalid Input) | 406 |
code_challenge_method missing | 1605 (Invalid Input) | 406 |
code_challenge_method not "S256" | 1605 (Invalid Input) | 406 |
code_challenge not 43 characters | 1605 (Invalid Input) | 406 |
state missing | 1605 (Invalid Input) | 406 |
client_id not found | 1605 (Invalid Input) | 406 |
| Client is disabled | 1605 (Invalid Input) | 406 |
redirect_uri mismatch | 1605 (Invalid Input) | 406 |
Invalid resource indicator | 1605 (Invalid Input) | 406 |
access_mode non-empty and not "r", "rw" or "rwa" | 180242 | 406 |
account_settings non-empty and not "1" or "0" | 117053 | 406 |
| Internal storage failure | 1654 (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)
Request Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| auth_request_id | string | Yes | — | 64-character hex authorization request ID from the GET request |
| scopes | string | Conditional | — | JSON array of entity IDs or scope strings (e.g., "[12345, 67890]" or '["org:12345:rw"]'). Must be a non-empty JSON list when supplied, and is required for scope=org and scope=workspace |
| access_mode | string | No | the value stored at initiate | "r", "rw" or "rwa" — may only narrow the initiate value, never widen it |
| account_settings | string | No | "0" | "1" appends userdetails:*:rw server-side, and only when the authorization was initiated with account_settings=1 |
| agent_name | string | No | — | Override the agent display name from the GET request (max 128 characters) |
Consent rules — scopes, access mode and account settings:
access_modemay only be narrower than the value stored at initiate. Omit it to take the stored value. A wider value is refused with40310768(error.params.reason:access_mode_exceeds_initiate); an unrecognised value is406161579.scopes, when supplied, must be a non-empty JSON list.[],{},"", a scalar, a nested array or malformed JSON is refused with406168998(or128552when it parses to nothing usable).- When
scopesis omitted,scope=organdscope=workspacenow require a selection —406121987, "The scopes parameter is required for this authorization request." The wildcard scope types (user,all_orgs,all_workspaces,all_shares) resolve to their own default set at the consented access mode, and an unrecognised scope word resolves to theusertype, as before. - For
scope=organdscope=workspace, an explicitscopeslist may narrow within the initiated scope type but may not name a different entity type (406149822). Ascope=userauthorization is the whole account, so its list may name any entity type the human holds. - Every granted scope is capped at the stored access mode; a broader one is refused with
40310768/access_mode_exceeds_initiate. - A client-supplied
userdetailsscope is always refused (same code and reason). Clients must never putuserdetails:*:rwinscopesthemselves. account_settings=1appendsuserdetails:*:rwserver-side, and only when the initiate request carriedaccount_settings=1; otherwise the consent is refused with40310768/access_mode_exceeds_initiate.- The consent request itself must be made with an account-level credential — a narrowed credential is refused with
40310175— and the granted set may not exceed that credential (40310768/scope_exceeds_issuer). - The 20-scope cap still applies after the server-side append —
406157349.
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,
"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 | 1650 (Authentication Invalid) | 401 |
auth_request_id missing | 1605 (Invalid Input) | 406 |
| Request expired or not found | 1609 (Not Found) | 404 |
Invalid access_mode | 1605 (Invalid Input) | 406 |
| Scope validation failed | 1605 (Invalid Input) | 406 |
Unrecognised access_mode value | 161579 | 406 |
access_mode broader than the initiate value, a client-supplied userdetails scope, or account_settings=1 without it at initiate | 10768 (access_mode_exceeds_initiate) | 403 |
scopes not a non-empty JSON list | 168998 / 128552 | 406 |
scopes omitted for scope=org or scope=workspace | 121987 | 406 |
| Consent made with a narrowed (non-account-level) credential | 10175 | 403 |
| Granted scopes exceed the consenting credential | 10768 (scope_exceeds_issuer) | 403 |
| More than 20 scopes after the server-side append | 157349 | 406 |
| Internal processing error | 1654 (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
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,
"valid": true,
"client_name": "My App",
"scope": "user",
"agent_name": "My MCP Agent",
"redirect_mode": "redirect",
"redirect_uri": "http://localhost:8080/callback",
"resource": "https://mcp.fast.io/mcp",
"access_mode": "rw",
"account_settings": false
}
resource is the RFC 8707 resource indicator the client supplied at initiate (or null), access_mode is the requested access mode ("r", "rw" or "rwa", or null when the client did not supply one), and account_settings is a boolean that is always present. All three are returned server-authoritatively from the stored authorization request, so consent screens can render them without trusting URL parameters.
Show the account-settings toggle only when account_settings is true, and default it to off. When it is false the authorization was not initiated for account settings, and consent cannot grant them.
Response (invalid or expired — 200 OK):
{
"result": true,
"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) |
| response.resource | string or null | The RFC 8707 resource indicator recorded at initiate, or null when the client supplied none. Always present when valid is true |
| response.access_mode | string or null | Requested access mode — "r" (read-only), "rw" (read-write) or "rwa" (read-write-administer) — or null when the client supplied none. It is the ceiling for consent. Always present when valid is true |
| response.account_settings | boolean | Whether the authorization was initiated with account_settings=1, i.e. whether account-settings access (userdetails:*:rw) may be granted at consent. Always present when valid is true; render the toggle only when it is true, defaulted to off |
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
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. Store securely. Returned unchanged on refresh (no rotation). |
| scope | string | The legacy scope word that was granted (e.g. user, org) |
| scopes | string | JSON-encoded array of scope strings in entity_type:entity_id:access_mode format. Emitted for every grant — a plain scope=user grant is stored explicitly as ["user:*:rw"] and reports it here. Persist this, not scope |
| agent_name | string | Agent display name (v2.0 only, present when set during authorization) |
scopeversusscopes.scope(singular) is the legacy scope word;scopes(plural) is a JSON-encoded string listing the granted entity scopes. Persistscopes, notscope.
scopesis now returned for every grant, including a plainscope=usergrant that previously carried no list — such a grant is stored explicitly as["user:*:rw"]. Clients must not treat a missingscopesas “this is a full session”, and must not branch onauth_type === "jwt_v1": those grants now reportjwt_v2at introspection.This response carries no
adminand nolegacyfield — derive administrative capability fromGET /current/auth/scopes/.
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. The refresh_token is long-lived and is returned unchanged (no per-refresh rotation).
Refresh narrows a demoted administrative grant. A grant naming a concrete entity at rwa whose human is no longer an administrator of it comes back as rw, and the response's scopes echoes the narrowed set — which is then what the session holds. Wildcard rwa grants are never narrowed this way; they are capped live on every request against the human's role. Losing access to an entity entirely still revokes, as before.
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 |
Per-user active-session cap reached on authorization-code exchange (error_description begins "You have reached the maximum number of active connections") — revoke an existing session via DELETE /current/oauth/sessions/{session_id}/ and retry | 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
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
}
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 refreshes.
GET /current/oauth/sessions/
List all active (non-expired, non-revoked) OAuth sessions for the authenticated user.
/current/oauth/sessions/
Auth: Required (Bearer JWT)
curl -X GET "https://api.fast.io/current/oauth/sessions/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"sessions": [
{
"session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"client_id": "my-app",
"scopes": ["org:12345:rw"],
"agent_name": "My MCP Agent",
"device_name": "Chrome on macOS",
"device_type": "desktop",
"ip_address": "203.0.113.42",
"last_used": "2026-01-22 14:00:00 UTC",
"created": "2026-01-21 14:00:00 UTC",
"expires": "2036-01-21 14:00:00 UTC",
"admin": false,
"legacy": false
}
],
"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[].scopes | array | Granted scope strings in entity_type:entity_id:access_mode format. A scope=user grant is now stored explicitly as ["user:*:rw"]; an empty array means a legacy session that declares no scopes claim at all. |
| response.sessions[].agent_name | string or null | Agent display name if set during authorization, otherwise null |
| 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 from the most recent refresh |
| response.sessions[].last_used | string | Datetime of last token refresh (YYYY-MM-DD HH:MM:SS UTC) |
| response.sessions[].created | string | Datetime when the session was created (YYYY-MM-DD HH:MM:SS UTC) |
| response.sessions[].expires | string | Datetime when the session expires (YYYY-MM-DD HH:MM:SS UTC) |
| response.sessions[].admin | boolean | Whether the session carries at least one rwa scope, i.e. whether it can perform administrative operations (still capped by the human's live role) |
| response.sessions[].legacy | boolean | Whether the session declares no scopes claim at all. A legacy OAuth session behaves as user:*:rw: whole-account read and write, no administration, no account settings. legacy never implies admin, and admin never implies legacy |
| response.count | integer | Total number of active sessions |
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | 1650 (Authentication Invalid) | 401 |
| Internal error | 1654 (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)
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,
"session": {
"session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"client_id": "my-app",
"scopes": ["org:12345:rw"],
"agent_name": "My MCP Agent",
"device_name": "Chrome on macOS",
"device_type": "desktop",
"ip_address": "203.0.113.42",
"last_used": "2026-01-22 14:00:00 UTC",
"created": "2026-01-21 14:00:00 UTC",
"expires": "2036-01-21 14:00:00 UTC",
"admin": false,
"legacy": false
}
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | 1650 (Authentication Invalid) | 401 |
session_id missing | 1605 (Invalid Input) | 406 |
session_id not 32 hex chars | 1605 (Invalid Input) | 406 |
| Session not found or wrong user | 1609 (Not Found) | 404 |
| Internal error | 1654 (Internal Error) | 500 |
PATCH /current/oauth/sessions/{session_id}/
Update the device_name, agent_name and/or scopes 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)
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) |
| scopes | string | No | JSON-encoded list of scope strings, the same shape as an API key's. Must be narrower than or equal to both what the session already holds and what the calling credential holds |
At least one of device_name, agent_name or scopes must be provided, so a scopes-only request is valid.
Narrowing only — there is no widening path. Submitted scopes are checked against the session's current scopes and against the credential making the request; a broader set is refused with 403 and 10768 (error.params.reason: scope_exceeds_issuer). An empty list ([]) is refused with 406 136396 — to end a session entirely use DELETE /current/oauth/sessions/{session_id}/. A narrowing takes effect for the access token at the next refresh.
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,
"session": {
"session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"client_id": "my-app",
"scopes": ["org:12345:rw"],
"agent_name": "My MCP Agent",
"device_name": "My Work Laptop",
"device_type": "desktop",
"ip_address": "203.0.113.42",
"last_used": "2026-01-22 14:00:00 UTC",
"created": "2026-01-21 14:00:00 UTC",
"expires": "2036-01-21 14:00:00 UTC",
"admin": false,
"legacy": false
}
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | 1650 (Authentication Invalid) | 401 |
session_id missing | 1605 (Invalid Input) | 406 |
session_id not 32 hex chars | 1605 (Invalid Input) | 406 |
None of device_name, agent_name or scopes provided | 1605 (Invalid Input) | 406 |
scopes broader than the session or than the calling credential | 10768 (scope_exceeds_issuer) | 403 |
scopes submitted as an empty list ([]) | 136396 | 406 |
scopes no longer grantable to this user | 176886 | 406 |
device_name exceeds 128 chars | 1605 (Invalid Input) | 406 |
agent_name exceeds 128 chars | 1605 (Invalid Input) | 406 |
| Session is revoked | 1605 (Invalid Input) | 406 |
| Session not found or wrong user | 1609 (Not Found) | 404 |
| Internal error | 1654 (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)
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,
"message": "Session has been revoked."
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | 1650 (Authentication Invalid) | 401 |
session_id missing | 1605 (Invalid Input) | 406 |
session_id not 32 hex chars | 1605 (Invalid Input) | 406 |
| Session not found or wrong user | 1609 (Not Found) | 404 |
| Internal error | 1654 (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)
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 when exclude_current is set. If omitted (or exclude_current is unset), all sessions are revoked. |
# 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,
"message": "All sessions have been revoked."
}
Or when exclude_current is used:
{
"result": true,
"message": "All other sessions have been revoked."
}
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | 1650 (Authentication Invalid) | 401 |
| Internal error | 1654 (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)
curl -X GET "https://api.fast.io/current/auth/scopes/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"auth_type": "jwt_v2",
"scopes": ["org:12345:rw", "org:67890:rw"],
"scopes_detail": [
{
"entity_type": "org",
"entity_id": "12345",
"access_mode": "rw",
"admin": false,
"name": "Acme Corp",
"domain": "acme"
},
{
"entity_type": "org",
"entity_id": "67890",
"access_mode": "rw",
"admin": false,
"name": "Beta Inc",
"domain": "beta"
}
],
"is_agent": true,
"agent_name": "My MCP Agent",
"full_access": false,
"admin": false,
"legacy": false
}
Response (200 OK — API-key auth with scopes and expiration):
{
"result": true,
"auth_type": "api_key_scoped",
"scopes": ["org:12345:rw"],
"scopes_detail": [
{
"entity_type": "org",
"entity_id": "12345",
"access_mode": "rw",
"admin": false,
"name": "Acme Corp",
"domain": "acme"
}
],
"is_agent": true,
"agent_name": "CI Pipeline",
"full_access": false,
"admin": false,
"legacy": false,
"expires": "2026-12-31 23:59:59 UTC"
}
Response Fields:
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| response.auth_type | string | "jwt_v2" (scoped JWT), "jwt_v1" (legacy JWT), "api_key" (legacy API key that declares no scopes claim), or "api_key_scoped" (API key whose stored token record has a non-empty scopes claim) |
| response.scopes | array | Scope strings in entity_type:entity_id:access_mode format. Empty only for a legacy credential that declares no scopes claim at all (a v1.0 JWT or a pre-scopes API key). |
| response.scopes_detail | array | Hydrated scope objects with entity names and metadata, one per entry in scopes. Empty when scopes is empty. |
| 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 | Whether the credential is account-wide and may write — user:*:rw or user:*:rwa. true for legacy (unscoped) API keys and browser login sessions; false for user:*:r and for entity-scoped credentials. It is not an administration flag — read admin for that |
| response.admin | boolean | Whether the credential can perform administrative operations: true for a browser login session and for any credential holding an rwa scope (still capped by the human's live role). Always present |
| response.legacy | boolean | Whether the credential declares no scopes claim at all — a pre-scopes API key, a pre-scopes OAuth session, and every browser login session. Always present. legacy never implies admin, and admin never implies legacy |
| response.expires | string | API-key auth only: token expiration timestamp in canonical YYYY-MM-DD HH:MM:SS UTC format. Omitted entirely when the token has no expiration set or when auth is JWT-based. |
Scope Detail Fields:
Each entry in scopes_detail contains entity-specific fields:
| Field | Type | Present For | Description |
|---|---|---|---|
| entity_type | string | All | user, org, workspace, share, fileshare, memory, or userdetails |
| entity_id | string | All | Numeric ID or * for wildcard |
| access_mode | string | All | r (read), rw (read/write) or rwa (read/write/administer) |
| admin | boolean | All | true when this entry's access mode is rwa, so a client need not re-parse the scope string |
| 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 |
Labels follow the access mode: user:*:rw renders as "Full Access", user:*:rwa as "Full Access (Admin)", user:*:r as "Entire account (Read Only)" and userdetails:*:rw as "Account settings"; other wildcards render in the same style, e.g. "All Organizations (Read/Write/Admin)".
How the flags combine:
| Credential | full_access | admin | legacy | auth_type |
|---|---|---|---|---|
user:*:rwa | true | true | false | jwt_v2 / api_key_scoped |
user:*:rw | true | false | false | jwt_v2 / api_key_scoped |
user:*:r | false | false | false | jwt_v2 / api_key_scoped |
Scoped, e.g. org:123:rwa | false | true | false | jwt_v2 / api_key_scoped |
| Legacy key with no scopes | true | false | true | api_key |
| Browser login session | true | true | true | jwt_v1 |
Error Responses:
| Scenario | Error Code | HTTP Status |
|---|---|---|
| Not authenticated | 1650 (Authentication Invalid) | 401 |
| Wrong HTTP method | 109463 | 405 |
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 a new access_token; the same refresh_token is returned unchanged
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 | 1671 (Rate Limited) | 429 |