OAuth authorization server for MCP
Lumio is the OAuth authorization server for its remote MCP endpoint. The production issuer is https://api.lumio.vision; the protected resource is https://api.lumio.vision/mcp. Staging uses https://lumio.api.staging.zaflun.dev and its /mcp resource. OAuth protocol routes are at the API root, while the first-party consent and connection-management API is under /v1.
Discovery and client identity
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-protected-resource/mcp | Resource, authorization server and supported scopes |
GET /.well-known/oauth-authorization-server | Issuer, endpoints, grant types, S256, client metadata support and scopes |
POST /oauth/register | Dynamic client registration (DCR), public clients only |
GET /oauth/authorize | Authorization-code request and redirect to Lumio consent |
POST /oauth/token | Code exchange or refresh-token rotation |
POST /oauth/revoke | Token revocation |
An operator may register a client through Admin → OAuth Clients. Dynamic registration accepts up to five HTTPS or loopback redirect URIs and has per-IP and global rate limits. A client may also identify itself through an HTTPS client ID metadata document (CIMD); Lumio fetches it with pinned destination checks, a short timeout and a size cap. A DCR request or CIMD document must be a public client (token_endpoint_auth_method absent or none). If it lists grant_types, the list must contain authorization_code; if it lists response_types, the list must contain code. Other values Lumio does not support, such as urn:ietf:params:oauth:grant-type:jwt-bearer, are ignored, and the client is stored with authorization_code and refresh_token (RFC 7591 §3.2.1). An admin client is stricter: its grant types are refused if they contain anything else. A refused CIMD document is cached as unusable for five minutes, and /oauth/authorize sends the browser to /oauth/error?error=invalid_client. DCR and CIMD clients are marked Not verified. An operator can disable a client; that revokes its grants.
Authorization and consent
The authorization-code flow requires PKCE S256; the requested resource must match the configured MCP resource exactly. Every token family stores that resource as its audience (oauth_token_families.audience). /mcp refuses an access token whose family was minted for another resource with invalid_token. The refresh grant refuses such a refresh token with invalid_grant, and it neither consumes the token nor treats the attempt as reuse. Changing [mcp] resource, or server.public_url while resource is empty, therefore invalidates every issued token, and users reconnect their AI apps. The browser is sent to the ID app's /oauth/consent page. Unknown, blocked or unverified redirect destinations go to Lumio's own /oauth/error?error=<code> page, without client-controlled text or a link back to the client.
The error page translates only the allowlisted codes invalid_request, invalid_client, invalid_client_metadata, unauthorized_client, slow_down, temporarily_unavailable and server_error. An unknown code displays generic error text; it does not direct the user back to an unverified client.
The consent page requires an interactive Lumio session. It displays the client's identity and requested rights in groups, and the user's eligible channels. It can narrow, never expand, the requested scopes. Only the Read group and eligible channels the user owns are selected initially; member channels need an explicit tick. Each of the channels, rights and high-risk sections offers Select all / Deselect all when it has at least two selectable choices. The controls skip ineligible channels and do not enable the separate write opt-in for a channel the user does not own; that switch starts off. High-risk groups require a recent aiAppConsent step-up. With 2FA enabled, only a passkey, authenticator app code or recovery code confirms it; without 2FA, an existing platform sign-in or email password can confirm it. The page identifies the AI provider when chat or event reading is selected and links to the privacy notice.
The consent handle is bound to the first signed-in user who opens it. An accepted decision returns a one-use code valid for 120 seconds. The grant and first token family are created when that code is redeemed, not merely when consent is displayed. POST /oauth/token returns a one-hour lm_oat_* access token and, when the client supports it, a rotating lm_ort_* refresh token. Ordinary grants have a 90-day absolute and 30-day sliding lifetime; high-risk grants have 30 and 7 days. Reusing a spent refresh token revokes the grant, switches off its automations and sends a security notice.
Request-time authorization
An MCP call uses only lm_oat_* in the Authorization header. Every channel call intersects the grant's scopes with the AI-tool allowlist and the user's current role in that channel. The channel must still belong to the grant, allow feature:mcp_server, and give the user apikeys:create. The tool's own permission and feature gate also apply. A role downgrade takes effect on the next call; an unreadable permission ceiling fails closed. The user-level lumio_login_connections call skips channel context: it accepts no channel and returns only the grant owner's sign-in accounts. MCP tokens cannot create API keys, popout tokens or further grants, and are refused by /v1.
The REST and GraphQL APIs expose the same first-party consent and connection operations. They require an interactive session and share the service-level guards and validation. The AI apps guide covers the user flow.