Aller au contenu principal
Cette page vous a-t-elle été utile ?

OAuth Authorization Server (MCP)

PrimeCal runs a self-contained OAuth 2.1 authorization server purpose-built for the MCP connector use case: public clients only, PKCE mandatory, RFC 7591 dynamic registration. It exists so any spec-compliant MCP host can add PrimeCal without a developer manually minting an ag_sk_... key.

See Agent API for the full request/response reference and curl walkthrough, and Connecting via OAuth for the end-user-facing flow. This page describes how the authorization server behaves, for developers building or operating an MCP host.

Why Public Clients Only, PKCE Mandatory​

No client_secret is ever issued, and client registration always reports token_endpoint_auth_method: "none". MCP hosts are typically desktop apps, browser extensions, or other clients that cannot keep a confidential secret, so PKCE is the OAuth 2.1-recommended substitute at the token endpoint. Only the S256 challenge method is accepted, both on the authorization request and in the discovery metadata's code_challenge_methods_supported; plain is rejected.

Grant Flow, Step by Step​

  1. POST /api/oauth/register — public. Every redirect_uris entry must use https://, or be a loopback address (http://127.0.0.1 or http://localhost) for native and desktop clients (RFC 8252). The response carries a UUID client_id and no secret.
  2. GET /api/oauth/authorize — public, spec-mandated entry point. The client_id must be known, and the redirect_uri must match one of the client's registered URIs exactly, with no wildcards, so the endpoint cannot be used as an open redirect. It then issues a 302 to PrimeCal's consent page at /oauth/consent with the same query string appended. This endpoint never renders UI or checks who the caller is; the consent page does both.
  3. GET /oauth/consent (the PrimeCal web app) — uses the user's normal sign-in. If the user is signed out, they sign in first, and the pending authorization request survives that detour. Once signed in, the page calls the two endpoints below.
  4. GET /api/oauth/consent-details — requires a signed-in session and the agent feature on the account's plan. Re-validates client_id and redirect_uri, then returns the client's display details plus the permission groups the signed-in user is allowed to grant. A group the user cannot grant (full access, for example, is reserved for administrators) is never listed.
  5. The user picks a permission group and approves, which calls POST /api/oauth/consent (also requires a signed-in session). On approval:
    • The server checks again that the user may grant the chosen group. The list in step 4 is a convenience, not the security boundary.
    • An agent is provisioned for the client. The first approval creates it; approving the same client again reuses that agent instead of creating a duplicate, reactivating it after a revoke and updating its permission group if that changed.
    • An authorization code is issued. It expires 60 seconds after issue, can be used once, and is bound to the client, the user, the agent, the exact redirect_uri and the PKCE code_challenge.
    • The browser is handed back to the client's own redirect_uri with the code appended, or with error=access_denied if access is not granted.
  6. POST /api/oauth/token, grant_type=authorization_code — public. The server checks that the client matches, the code has not been used, the code has not expired, the redirect_uri matches exactly, and PKCE holds: base64url(sha256(code_verifier)) must equal the code_challenge. The code is marked as used before anything is returned, so a retry with the correct verifier fails once the code has been used. On success the response contains an access token and a refresh token.
  7. POST /api/oauth/token, grant_type=refresh_token — refresh tokens rotate. The presented token is checked, and if it is valid and has not been used it is revoked and a fresh access and refresh token pair is issued. If a refresh token that was already rotated is presented again (which suggests it was stolen), every refresh token issued for that client and agent is revoked, and the caller receives invalid_grant, which forces a full re-authorization from step 2.

Errors from steps 6-7 use the RFC 6749 section 5.2 shape, { "error": ..., "error_description": ... }, not PrimeCal's usual { "success": false, "error": { "code", "message" } } envelope.

Using the Access Token​

Clients send the access token as a Bearer token in the Authorization header. When a request is rejected with 401, the response carries this challenge:

WWW-Authenticate: Bearer realm="PrimeCal MCP API", resource_metadata="<origin>/.well-known/oauth-protected-resource"

so spec-compliant MCP clients (RFC 9728) can discover the OAuth flow from a bare 401 instead of failing with no next step. <origin> is taken from the x-forwarded-proto and x-forwarded-host headers when they are present, and from the request itself otherwise, so the challenge is correct behind a reverse proxy.

Discovery Metadata​

The discovery endpoints are public and unauthenticated:

  • GET /.well-known/oauth-protected-resource (+ /*path catch-all) → { resource, authorization_servers, bearer_methods_supported, resource_documentation }
  • GET /.well-known/oauth-authorization-server (+ /*path catch-all) → { issuer, authorization_endpoint, token_endpoint, registration_endpoint, response_types_supported, grant_types_supported, code_challenge_methods_supported, token_endpoint_auth_methods_supported, scopes_supported }

Both build their origin from the request, honouring the forwarded headers described above and falling back to api.primecal.eu if no host header is present at all. The /*path catch-all variants exist because some MCP clients probe suffixed discovery paths (e.g. /.well-known/oauth-authorization-server/api/mcp) per RFC 8414 §3.1.