OIDC & OAuth 2.0 Compliance
Complete reference of OAuth 2.0 and OpenID Connect standards implemented by AuthPI.
Last updated 2026-09-16
AuthPI implements OAuth 2.0 and OpenID Connect (OIDC) following the relevant RFCs and specifications. This document details which standards are supported, how they’re implemented, and any deviations or limitations.
Standards Overview
| Standard | Status | Notes |
|---|---|---|
| OAuth 2.0 (RFC 6749) | Partial | Authorization code and refresh token grants |
| OAuth 2.0 Bearer Tokens (RFC 6750) | Full | |
| PKCE (RFC 7636) | Full | S256 only, required for public clients |
| Token Introspection (RFC 7662) | Full | |
| Token Revocation (RFC 7009) | Full | |
| JWT Access Tokens (RFC 9068) | Full | |
| Token Exchange (RFC 8693) | Partial | Constrained, policy-driven access-token exchange for trusted confidential service clients |
| Resource Indicators (RFC 8707) | Partial | Authorization-code and refresh grants bind repeated resource values; client_credentials supports one resource |
| Client ID Metadata Documents (IETF draft) | Opt-in | Public authorization-code clients; disabled by default per issuer |
| OpenID Connect Core 1.0 | Full | |
| OpenID Connect Discovery 1.0 | Full | |
| OpenID Connect Session Management 1.0 | Partial | Front-channel and back-channel logout |
OAuth 2.0 (RFC 6749)
Supported Grant Types
Authorization Code Grant (RFC 6749 Section 4.1)
The primary grant type for web applications and native apps. The flow works as follows:
- Client redirects user to
/authorizewithresponse_type=code - User authenticates and consents
- AuthPI redirects back with an authorization code
- Client exchanges code for tokens at
/token
GET /authorize?
response_type=code&
client_id=c_xxx&
redirect_uri=https://app.example.com/callback&
scope=openid%20profile%20email&
state=abc123
Refresh Token Grant (RFC 6749 Section 6)
Exchange a refresh token for new access and refresh tokens:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=eyJ...&
client_id=c_xxx&
scope=openid profile
The optional scope parameter may narrow the granted scope for the issued access token (RFC 6749 §6) — it must be a subset of the original grant, or the request fails with invalid_scope. Omit it to receive the full grant. The token response includes the effective scope, and the rotated refresh token always retains the complete original grant, so narrowing one refresh does not shrink later ones.
AuthPI implements refresh token rotation—each refresh returns a new refresh token, and the old one is invalidated. This improves security by limiting the window for token theft. Granted scopes and organization memberships are re-applied on every refresh: scopes from the grant of record, memberships fresh from the directory.
If the original authorization request used org=org_..., refresh keeps that same selected-organization restriction. The refresh request itself cannot add or change org.
Client Credentials Grant (RFC 6749 Section 4.4)
Machine-to-machine authentication with no user involved:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
scope=read:data
The client authenticates via HTTP Basic (client_secret_basic) or client_id + client_secret form parameters (client_secret_post).
- Available to M2M clients (
application_type: "m2m", confidential, withclient_credentialsin theirgrant_types) and to agents (agt_...identities authenticating with a secret verifier). - The response contains an access token only — no refresh token and no ID token. The
openidscope is ignored for this grant. - If no
scopeis requested, the token carries the client’s (or agent’s) full allowed scope set. Requesting a scope outside the allowed set fails withinvalid_scope. - The optional
resourceparameter sets the token’saudclaim; otherwise the audience defaults to the client id. - Token lifetime: the client’s
default_access_token_age(default 30 minutes). Agent tokens are fixed at 5 minutes so scope changes propagate quickly.
Note: the client credentials grant issues OAuth access tokens for calling your own APIs. To call the AuthPI Core (admin) API from a backend, use org API keys instead.
Token Exchange Grant (RFC 8693, constrained profile)
AuthPI supports access-token delegation within an issuer. A confidential service exchanges a human user’s token for a token intended for another resource, under an explicit source and target policy. You can use this capability with your own issuer, MCP server, and API; it is not limited to AuthPI’s own services. The destination API must independently trust that issuer and enforce its authorization claims. Sharing the AuthPI platform does not establish trust between issuers.
For setup and a complete request sequence, see Let your MCP server call your API for a user. For AuthPI’s own issuer and resources, see Use AuthPI MCP.
Participants and client policy
The human is the token’s subject. The exchange actor is a separate registered, active, confidential service client in the same issuer, with application_type: "m2m", only the token-exchange grant, and empty response types and redirect URIs. The client’s internal or external classification is descriptive; neither grants exchange authority.
The actor needs settings.openid.token_exchange with non-empty subject_resources and target_resources lists. Each entry contains an exact resource URI and permitted scopes. Creating, modifying, or disabling that capability requires account-scoped issuers.clients:manage. Ordinary RFC 8707 allowed_resources entries never grant exchange authority; the actor’s general settings.scopes is not a substitute for either exchange policy list. Resource identifiers use the RFC 8707 validation and matching rules.
A public client, CIMD client, or agent identity cannot be the exchange actor. The original OAuth client that obtained the user’s subject token may be a public or CIMD client; it does not hold the actor’s secret. Cross-issuer subjects, tokens that already contain act, actor tokens, delegation chains, and refresh- or ID-token subjects are not supported.
Request
Send a form-encoded request to the subject user’s issuer. Authenticate the actor with client_secret_basic; credentials in the form body are rejected.
POST /{issuer_id}/token
Authorization: Basic base64(c_service:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange&
subject_token=eyJ...&
subject_token_type=urn:ietf:params:oauth:token-type:access_token&
resource=https%3A%2F%2Fapi.example.com&
scope=documents%3Aread&
org=org_0gw3hcq8r2kfn7xj9tzm4be5a
| Parameter | Contract |
|---|---|
grant_type | Required: urn:ietf:params:oauth:grant-type:token-exchange. |
subject_token | Required human access token, with one source audience allowed by the actor policy. |
subject_token_type | Required: urn:ietf:params:oauth:token-type:access_token. |
requested_token_type | Optional; if present, must be the same access-token type. |
resource | Exactly one target resource, allowed by the actor policy. Repeated values are rejected even if identical. |
scope | Explicit, non-empty, space-separated scopes. Duplicate scopes are rejected. |
org | Required AuthPI extension: an organization in the subject token and still permitted by live membership and actor organization restrictions. If the subject has org_id, this must match it. |
Other parameters, including audience, actor_token, and body client credentials, are not part of this profile.
Authority and lifecycle
Exchange verifies the subject token and its revocation state, requires the user to still be active, and reloads the selected active organization membership. A valid token and cached membership claims are not enough for a blocked, suspended, or deleted user to obtain another delegated token.
Every requested scope must be present in the subject token’s top-level OAuth grant, the source policy, the target policy, and the user’s current effective membership authority for the selected organization. Output scopes use concrete resource[.subresource]*:action permissions with read, write, delete, or manage actions; documents:read is one example. Membership wildcards may authorize a concrete scope, but the OAuth grant and both policy lists must contain that concrete scope exactly. Role scopes, OIDC scopes, authpi:all, and wildcard output scopes are rejected. If any requested scope fails these checks, the entire request fails with invalid_scope; no partial grant is issued.
The response contains an access token only:
{
"access_token": "eyJ...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "documents:read"
}
The token preserves the human sub and the subject token’s sid and auth_time when present. It has a fresh jti, the requested target audience, and act.sub plus client_id identifying the service actor. It contains exactly the selected organization: that membership’s scopes array is reduced to the same scope set as the top-level scope. See Token claims.
token_exchange.max_access_token_age defaults to 300 seconds and accepts 30–300. The issued lifetime is capped by that policy, 300 seconds, and the subject token’s remaining lifetime; it can therefore be less than 30 seconds. No refresh token or ID token is issued. Subsequent exchanges recheck authority; already-issued tokens are not continuously refreshed membership snapshots.
For response statuses, causes, and recovery actions, see Error codes, especially invalid_client, unauthorized_client, invalid_grant, invalid_target, and invalid_scope.
Not Supported
The following grant types are not implemented:
- Implicit Grant (RFC 6749 Section 4.2): Deprecated; use authorization code with PKCE
- Resource Owner Password (RFC 6749 Section 4.3): Not recommended due to security concerns
- Device Authorization (RFC 8628): Not currently supported
Redirect URI matching
AuthPI matches authorization redirect URIs exactly, with one port exception for HTTP loopback callbacks used by public native clients and CIMD clients. This lets a desktop or CLI application choose an available local port for each sign-in, following RFC 8252 Section 7.3.
For a registered client, the exception requires confidential: false and settings.openid.application_type: "native". Configure the callback in settings.openid.redirect_uris. Public web clients, SPAs, confidential clients, and clients without an application type retain exact matching, including the port. Public clients must still use S256 PKCE.
Only the port may vary for HTTP callbacks on 127.0.0.1, [::1], or localhost; the scheme, hostname spelling, path, and query must match exactly. These hostnames are not interchangeable. Prefer the IP literals over localhost, as RFC 8252 Section 8.3 recommends. HTTPS redirects retain exact port matching.
For example, a registered callback or CIMD metadata entry of http://127.0.0.1/callback/my-client permits an authorization request using http://127.0.0.1:65090/callback/my-client. AuthPI preserves that full requested URI. The subsequent authorization-code token exchange must send the identical redirect_uri, including port 65090; changing or omitting that port is rejected.
Registered callback configuration
The console and Core API validate settings.openid.redirect_uris and settings.openid.post_logout_redirect_uris when creating a client or submitting changes to either list:
| Application type | Supported callback forms |
|---|---|
| Web or SPA | HTTPS URLs; HTTP on exact loopback hosts for local development, with exact port matching |
| Native | HTTPS URLs, HTTP loopback URLs, or private-use schemes based on a reversed domain name, such as com.example.app:/callback |
| M2M | No authorization redirect URIs |
Customer-owned and private-network HTTPS hosts are supported. Callback URLs must not contain credentials, fragments, whitespace, control characters, or backslashes. Remote HTTP URLs and schemes such as javascript:, data:, and file: are rejected. Native schemes without a domain component, such as myapp:, are rejected when saving settings; use a scheme based on a domain you control, following RFC 8252 Section 7.1.
An empty authorization callback list is allowed for an incomplete client. Existing client records remain readable. You can disable a client or edit unrelated properties without replacing unchanged legacy callbacks, and repair each callback list separately. Re-enabling a disabled client requires both lists to satisfy the current validation rules.
Client ID Metadata Documents
Client ID Metadata Documents (CIMD) let a public OAuth client use the exact HTTPS URL of a JSON metadata document as its client_id. AuthPI fetches and validates that document instead of looking up a stored Client resource. CIMD does not dynamically register the client or create a Client resource; see Registered Clients, CIMD, and Dynamic Registration for when to use each model.
CIMD is disabled by default. In the console, open an issuer’s Settings → Access → Client ID Metadata Documents (CIMD) and turn on Enable CIMD. Changes save automatically. The control requires issuer write access and is unavailable for suspended issuers. Use Organization access for CIMD clients to choose which memberships appear in tokens. Changing the enable switch preserves the saved organization policy; other OAuth settings are unchanged.
For MCP, also configure Protected resources on the same tab. CIMD enables client identification; it does not authorize an MCP resource or its scopes. Follow Secure your MCP server with AuthPI for the console walkthrough using your own issuer and MCP server.
You can also enable CIMD through the issuer settings API:
{
"settings": {
"security": {
"openid_policy": {
"client_id_metadata_document": {
"enabled": true,
"organization_policy": "all"
}
}
}
}
}
organization_policy defaults to all for newly created issuers. Existing issuers retain their saved policy. The console offers:
- All organizations (
all): include the user’s active organization memberships and roles, enabling account discovery and switching in MCP clients. - Selected organization only (
selected): include only an explicitly requested active membership. Without an organization selection, no memberships are included. - No organizations (
none): authenticate users without organization membership claims.
This policy applies to CIMD clients using this issuer. Sign in again after changing it to obtain a token with the updated claims. When CIMD is disabled, URL-shaped client IDs are rejected.
Client ID URL requirements
The client_id URL must:
- Use HTTPS and be no more than 1,000 characters.
- Have a non-root path.
- Have no username, password, fragment, IP-literal or local hostname, or
./..path segment.
Ports and query strings are allowed. They are identity-significant: AuthPI uses the exact URL string for document identity, caching, and consent, without normalizing it.
Supported metadata
AuthPI reads this metadata subset. Additional non-secret properties, such as client_uri and logo_uri, are accepted but ignored:
| Field | Requirement |
|---|---|
client_id | Required. Must exactly equal the URL used in the authorization request. |
client_name | Required, non-empty, at most 200 characters. |
redirect_uris | Required, with 1–100 entries. Every entry must satisfy the redirect policy below. |
token_endpoint_auth_methods_supported | Optional non-empty array of non-empty method names. When present, it must include none, which AuthPI selects. |
token_endpoint_auth_method | Required as none when the supported-methods array is absent. With the array present, this optional legacy preference does not override the array. |
grant_types | Optional; defaults to ["authorization_code"]. May also include refresh_token. |
response_types | Optional; defaults to ["code"]. No other response type is accepted. |
scope | Optional space-separated scope list, at most 1,000 characters. It narrows the scopes the client may request. |
id_token_signed_response_alg | Optional; EdDSA, ES256, or RS256. |
CIMD clients are always public authorization-code clients and must use S256 PKCE. Secret-bearing metadata, including client_secret, client_secret_expires_at, and registration_access_token, is rejected.
This accepts clients such as ChatGPT that advertise ["none", "private_key_jwt"] while setting their legacy preference to private_key_jwt. AuthPI selects none; it does not implement private_key_jwt client authentication. If the array is present but does not include none, the document is rejected even when the singular field says none. See OpenAI’s client registration documentation for ChatGPT’s metadata format.
{
"client_id": "https://mcp-client.example.com/oauth/client-metadata.json",
"client_name": "Example MCP client",
"redirect_uris": [
"https://mcp-client.example.com/oauth/callback",
"http://127.0.0.1:6274/callback"
],
"token_endpoint_auth_method": "none",
"token_endpoint_auth_methods_supported": ["none"],
"scope": "openid profile"
}
This example uses the omitted-field defaults for authorization code and code. To allow refresh tokens, add "grant_types": ["authorization_code", "refresh_token"]. The offline_access scope is valid only when refresh-token support is effective for both the document and issuer.
Every redirect URI in the document must be either:
- An absolute HTTPS URL, or
- An HTTP URL whose hostname is exactly
localhost,127.0.0.1, or[::1].
Credentials, fragments, malformed URLs, other HTTP hosts, and custom schemes such as javascript: or native-app schemes are rejected. Authorization requests must match one of the validated document values under the redirect URI matching rules, including the HTTP loopback port exception.
Fetching and caching
AuthPI fetches metadata only after validating the user’s AuthPI session, and connections are restricted to public network addresses. The fetch uses a five-second timeout, does not follow redirects, requires HTTP 200 with application/json or application/*+json, and limits the response body to 5 KiB. AuthPI does not fetch or render URLs referenced by metadata fields such as logos, policies, terms, or JWKS.
Validated documents are cached according to shared-cache Cache-Control semantics, for at most one hour. s-maxage takes precedence over max-age; without explicit freshness, the cache lifetime is five minutes. no-store, no-cache, private, zero freshness, and effective lifetimes below 60 seconds prevent caching; failed or invalid responses are never cached.
Authorization display
CIMD authorization always presents a confirmation screen showing the client name, the client-ID hostname, and the requested redirect hostname, including a non-default port. AuthPI warns when the selected redirect uses a loopback address and displays a stronger warning when every redirect in the metadata document is loopback-only. Treat a localhost callback as belonging to whichever local process is listening on that port, not as proof of the client’s identity.
PKCE (RFC 7636)
Proof Key for Code Exchange prevents authorization code interception attacks. AuthPI fully implements PKCE with the following behavior:
Requirements
| Client Type | PKCE Requirement |
|---|---|
| Public clients (SPAs, native apps) | Required |
| Confidential clients | Optional but recommended |
Supported Methods
Only S256 is supported. The plain method is rejected for security reasons.
// Generate code verifier (43-128 characters, URL-safe)
const verifier = generateRandomString(64);
// Generate code challenge
const challenge = base64url(sha256(verifier));
// Authorization request
GET /authorize?
...&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
// Token request includes verifier
POST /token
...&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Error Handling
| Scenario | Error |
|---|---|
Public client without code_challenge | invalid_request |
code_challenge_method=plain | invalid_request |
Invalid code_verifier at token exchange | invalid_grant |
Missing code_verifier at token exchange | invalid_grant |
Token Introspection (RFC 7662)
The introspection endpoint allows resource servers to validate tokens and retrieve their metadata.
Endpoint: POST /{issuer_id}/introspect
Request
POST /introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=eyJ...&
token_type_hint=access_token
The token_type_hint parameter is optional but helps AuthPI validate the token more efficiently. Accepted values: access_token, refresh_token.
Response (Active Token)
{
"active": true,
"scope": "openid profile email",
"client_id": "c_xxx",
"token_type": "Bearer",
"sub": "usr_xxx",
"aud": "c_xxx",
"iat": 1705330953,
"exp": 1705332753,
"iss": "https://idp.authpi.com/i_xxx",
"jti": "tok_xxx",
"sid": "ses_xxx",
"username": "user@example.com"
}
Response (Inactive Token)
{
"active": false
}
A token is inactive if it’s expired, revoked, malformed, or issued by a different issuer.
Access vs Refresh Token Introspection
Access tokens are validated by checking:
- JWT signature validity
- Expiration time
- Revocation status in the revocation list
Refresh tokens are validated by checking:
- JWT signature validity
- Session binding (must have valid
sid) - Token rotation (JTI must match current token)
Token Revocation (RFC 7009)
Revoke tokens when users log out or when tokens may be compromised.
Endpoint: POST /{issuer_id}/revoke
Request
POST /revoke
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
token=eyJ...&
token_type_hint=refresh_token
Response
The endpoint always returns 200 OK regardless of whether the token was valid or already revoked. This prevents information leakage about token validity.
Revocation Behavior
Refresh tokens: Revoked via the session manager. The token’s JTI is marked as revoked, and subsequent uses of the refresh token fail.
Access tokens: Added to a revocation list with a TTL matching the token’s remaining lifetime. Resource servers should introspect tokens to check revocation status.
JWT Access Tokens (RFC 9068)
AuthPI issues access tokens as JWTs following RFC 9068.
Header
{
"alg": "ES256",
"typ": "at+jwt",
"kid": "key_xxx"
}
Payload
{
"iss": "https://idp.authpi.com/i_xxx",
"sub": "usr_xxx",
"aud": "c_xxx",
"exp": 1705332753,
"iat": 1705330953,
"nbf": 1705330953,
"jti": "tok_xxx",
"client_id": "c_xxx",
"sid": "ses_xxx",
"scope": "openid profile email",
"auth_time": 1705330900
}
Supported Signing Algorithms
| Algorithm | Description |
|---|---|
| ES256 | ECDSA with P-256 curve (default) |
| RS256 | RSA with SHA-256 |
| EdDSA | Edwards-curve DSA |
The algorithm is configurable per client via settings.openid.response_signature_alg.
Resource Indicators (RFC 8707)
The OAuth resource parameter binds an authorization grant and its access tokens to the protected resource that will consume them. For authorization-code and refresh grants, send one form parameter per resource; do not combine several resource identifiers into a comma- or space-separated value.
GET /{issuer_id}/authorize?response_type=code
&client_id=c_xxx
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=read%3Areports
&state=abc123
&resource=https%3A%2F%2Fapi.example.com%2Freports
&resource=https%3A%2F%2Fapi.example.com%2Fexports
Repeat the resources at authorization-code exchange to select the full authorized set or a subset:
POST /{issuer_id}/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=...&
client_id=c_xxx&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
code_verifier=...&
resource=https%3A%2F%2Fapi.example.com%2Freports&
resource=https%3A%2F%2Fapi.example.com%2Fexports
The token request cannot introduce a resource that was absent from the authorization request. When resource indicators are optional and the code exchange omits resource, AuthPI selects the full set authorized by the original request.
A refresh request may select any subset of the immutable resource ceiling established by the original authorization request, including resources that were not selected at code exchange. Under an optional policy, omitting resource retains the session’s most recently selected set. A required policy rejects omission. Refresh rotation updates only that current selection; it never changes the original ceiling, so a later refresh may choose a different subset but can never add a new resource.
POST /{issuer_id}/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=...&
client_id=c_xxx&
resource=https%3A%2F%2Fapi.example.com%2Freports
One selected resource produces a string aud claim. Multiple selected resources produce an aud array. Array order has no meaning; resource servers must check membership rather than position. ID tokens and refresh tokens continue to use the client ID as their audience. See Token Claims for the complete claim rules.
This layered RFC 8707 policy applies to authorization-code and refresh-token flows. The client_credentials grant keeps its existing behavior: it accepts at most one resource string as the access-token audience, does not consult resource_indicators or allowed_resources, and reports malformed values as invalid_request rather than invalid_target.
Resource identifier rules
Each resource identifier must be a non-empty RFC 3986 absolute URI of at most 1,000 characters. It must use ASCII URI characters and valid percent escapes, with no whitespace, control characters, backslashes, or fragment, including an empty trailing #. One request may contain at most ten resource values. AuthPI preserves and compares the exact string—it does not normalize scheme or host casing, add or remove a trailing slash, decode percent escapes, or discard a query string. For example, these are different resources:
https://api.example.com/reportshttps://API.example.com/reportshttps://api.example.com/reports/https://api.example.com/reports?region=eu
Exact duplicate values collapse to one entry. Distinct values form a set for authorization checks, so request order is not authorization-significant and consumers must not depend on audience-array order.
Issuer and client policy
Resource authorization is layered. The issuer defines the available resources and the scopes valid for each one under settings.security.openid_policy.resource_indicators:
In the console, open the issuer’s Settings → Access → Protected resources. Choose Add resource, enter its exact Resource URL, and use Add scope for each allowed scope. Choose Save resources to persist additions, edits, or removals; Discard restores the saved list. This requires issuer write access and is unavailable for suspended issuers. The editor saves only resources, preserving required, allow_multiple, and the issuer’s other OAuth settings. An API update uses the following shape:
{
"settings": {
"security": {
"openid_policy": {
"resource_indicators": {
"required": false,
"allow_multiple": true,
"resources": [
{
"resource": "https://api.example.com/reports",
"scopes": ["read:reports"]
},
{
"resource": "https://api.example.com/exports",
"scopes": ["read:reports"]
}
]
}
}
}
}
}
resources accepts at most 100 unique entries, with at most 100 scopes on each entry. required defaults to false; when true, the authorization request and every authorization-code or refresh-token request must explicitly select a resource. Omission never falls back to the grant or session selection while this flag is enabled. allow_multiple defaults to true; when false, a request may select only one distinct resource. Every effective scope must appear in the scopes list of every selected resource. A scope that is valid for only one resource cannot be put into a token intended for several resources.
Enable required only after every affected client sends resource on authorization, code exchange, and refresh. Turning it on intentionally makes existing resource-less authorization families fail their next token request with invalid_target; it does not infer a resource from client configuration or a legacy audience.
Registered clients must also opt in to each resource under settings.openid.allowed_resources, which accepts at most 100 exact URIs:
{
"settings": {
"openid": {
"allowed_resources": [
"https://api.example.com/reports",
"https://api.example.com/exports"
]
}
}
}
The effective set for a registered client is the intersection of the issuer catalog and allowed_resources. An omitted or empty allowed_resources authorizes no RFC 8707 resource; it does not inherit the issuer catalog and does not implicitly authorize the client ID. A Client ID Metadata Document has no registered-client settings, so CIMD clients use the issuer catalog as their resource allowlist; the document’s own scope restrictions still apply.
Prior consent skips the consent screen only when the requested scopes and resources are both subsets of the same stored grant. Newly requested resources appear as read-only exact URIs on the consent screen; the browser can choose scopes but cannot add or replace resource values. A new consent decision replaces the previous scope-and-resource grant as one unit rather than unioning the two lists independently.
Errors
In authorization-code and refresh-token flows, malformed, missing-but-required, unknown, disallowed, excessive, or scope-incompatible resource selections fail with invalid_target. At the authorization endpoint, AuthPI redirects this error only after it has established that the supplied redirect_uri belongs to the client; the redirect includes error=invalid_target and the original state. If the client or redirect URI cannot be trusted—including an unresolved or invalid CIMD redirect—AuthPI returns a direct 400 JSON response and never redirects to the supplied URI.
At the token endpoint, invalid_target is always a direct 400 JSON response. Error descriptions do not enumerate the issuer’s resource catalog or the client’s allowlist.
Resource-rejection diagnostics classify failures as missing_required, malformed, too_many, multiple_disallowed, ambiguous_legacy, not_allowed, not_in_grant, scope_mismatch, policy_changed, or unknown. Diagnostic context is limited to the grant type, client kind, and resource count; requested resource URIs and configured allowlists are excluded.
The legacy audience parameter remains available for existing integrations and continues to use the registered client’s allowed_audiences. A request must not combine audience and resource; AuthPI rejects that ambiguity with invalid_request.
To migrate from audience:
- Add each protected-resource URI and its valid scopes to the issuer’s
resource_indicators.resources. - Add the same exact URI to each registered client’s
allowed_resources. - Send
resourceat both authorization and code exchange, then on refresh when selecting a subset. - Update resource-server verification to accept
audas either a string or an array containing its identifier. - Stop sending
audience; never send both parameters during a staged rollout.
Enable required resource selection
Start with required: false. Configure the issuer resource catalog and each registered client’s allowed_resources, then update every affected client to send exact configured resource values during authorization and permitted subsets during code exchange or refresh. Verify those requests before changing the issuer policy to required: true.
After required is enabled, existing authorization families that were created without a resource fail their next code-exchange or refresh request with invalid_target. Start a new authorization flow for those sessions. During a staged migration, send either audience or resource, never both.
MCP resource profile
For your MCP server, use its canonical resource URI when obtaining the user’s access token through authorization code and PKCE. Configure that URL and its permitted scopes in Settings → Access → Protected resources, following the console walkthrough. This authorizes access to your MCP server; token exchange is optional and only needed when it calls another API on the user’s behalf.
For a resource at https://mcp.example.com/mcp, the equivalent API patch is below. Include offline_access only if your client requests refresh access. When updating an existing issuer through the API, include its other resource entries too: resources replaces the list.
{
"settings": {
"security": {
"openid_policy": {
"resource_indicators": {
"resources": [
{
"resource": "https://mcp.example.com/mcp",
"scopes": ["openid", "offline_access", "documents:read"]
}
]
}
}
}
}
}
Registered consumer clients must permit the requested application scopes in settings.scopes and include the exact string https://mcp.example.com/mcp in settings.openid.allowed_resources; CIMD clients are governed by the issuer entry. Variants such as https://mcp.example.com, https://mcp.example.com/mcp/, or a differently cased hostname do not match. Clients must send the exact configured value at authorization, code exchange, and refresh.
Requiring resource selection for every client (required: true) or limiting every request to one resource (allow_multiple: false) are separate, issuer-wide API settings. They are not prerequisites for enabling CIMD or adding an MCP resource. Before changing them, follow Enable required resource selection.
To use that MCP token at another API, follow MCP token exchange. AuthPI MCP uses AuthPI’s own resource identifiers and issuer.
Scope titles and consent groups
Each issuer can describe its applications’ scopes through settings.security.openid_policy.scope_presentation. The same presentation is used for registered clients and CIMD clients. Scope names remain exact identifiers: titles, descriptions, and groups do not define permissions, expand wildcards, or change the scopes in a grant or token. Protected-resource and client scope policies still apply.
In the console, use Settings → Access → Scope titles and consent groups. The MCP authorization guide walks through the editor. Through the Core API, update the issuer with PATCH /v1/accounts/{account_id}/issuers/{issuer_id} and issuer write access (issuers:write). For example:
{
"settings": {
"security": {
"openid_policy": {
"scope_presentation": {
"scopes": {
"documents:read": {
"title": "Read documents",
"description": "Read the documents you can access.",
"source_language": "en"
},
"documents:write": {
"title": "Edit documents",
"description": "Create and update documents you can access.",
"source_language": "en"
}
},
"groups": [
{
"id": "documents",
"title": "Documents",
"description": "Read and edit your documents.",
"source_language": "en",
"scopes": ["documents:read", "documents:write"]
}
]
}
}
}
}
}
| Field | Contract |
|---|---|
scopes | Map of exact scope names to display text, with at most 200 entries. Defaults to {}. |
title | Required on each scope entry and group; 1–100 characters after trimming. |
description | Optional on each scope entry and group; 1–500 characters after trimming when supplied. |
source_language | Required on each scope entry and group; a BCP 47 language tag of 2–63 characters, such as en or fr-CA. Saved tags use canonical casing. |
groups | At most 40 groups. Defaults to []. |
Group id | Unique identifier, 1–100 characters, starting with a lowercase letter and containing only lowercase letters, digits, _, or -. |
Group scopes | Between 1 and 200 exact scope names. A scope may appear in only one group, once. Members do not need their own entry in the display-text map. |
Supplying scope_presentation replaces the complete presentation, including both the scope map and group list. Include every entry you want to retain. Omit the property to leave it unchanged; send {"scopes": {}, "groups": []} as its value to clear it. Other issuer settings are unaffected. Use the issuer’s current ETag with If-Match when updating an existing configuration; the console does this automatically.
The consent screen shows requested permissions except openid, which is implicit in sign-in and remains in the submitted grant. It is excluded from permission counts, and a group containing only openid is hidden. If a request contains just documents:read, the example group shows only that permission. Expanding a group reveals each requested scope’s exact name and display text. Its checkbox selects or deselects only newly requested optional scopes; previously granted scopes and required openid remain selected. New permissions remain identified when a group also contains previously granted permissions. Consent submits the selected individual scope strings, never a group ID.
A configured scope entry supplies its title and optional description. For a scope without an entry, the IdP uses settings.security.openid_policy.scope_descriptions[scope] if present, then its built-in description for a standard OIDC scope, then Access to {scope}. Clearing the new presentation therefore restores this fallback behavior without removing legacy descriptions.
source_language identifies the language of the saved text. Each entry may use a different source language. The IdP displays that text as supplied; it does not currently generate translations or select translated variants from the user’s locale.
Selected Organizations
AuthPI supports an optional org parameter on the authorization request to issue tokens restricted to one organization:
GET /authorize?
...&
org=org_0gw3hcq8r2kfn7xj9tzm4be5a
The selected organization is stored with the authorization code and resulting session. Authorization-code exchange and refresh do not accept org; they use the trusted value from the authorization flow. The constrained RFC 8693 grant is the exception: it requires org to select one organization already present in the subject token and still active in the directory.
Validation happens before tokens are issued:
- If the client has
settings.restrictions.organizations.policy: "none", selected-org requests fail withinvalid_request. - If the client has
policy: "allowlist", the requested org must appear inallowed_org_ids. - If the user is not an active member of the selected org, the authorization-code exchange fails with
invalid_grant.
OpenID Connect Core 1.0
AuthPI is a fully compliant OpenID Connect Provider (OP).
ID Token
ID tokens are JWTs containing identity claims about the authenticated user.
Required claims:
| Claim | Description |
|---|---|
iss | Issuer identifier URL |
sub | Subject identifier (user ID) |
aud | Audience (client ID) |
exp | Expiration time |
iat | Issued at time |
Authentication claims:
| Claim | Description |
|---|---|
auth_time | Time of authentication |
nonce | Client-provided nonce (if sent in request) |
acr | Authentication context class reference |
amr | Authentication methods used |
at_hash | Access token hash |
Profile claims (when requested via scopes):
| Scope | Claims |
|---|---|
profile | name, given_name, family_name, picture, locale |
email | email, email_verified |
phone | phone_number, phone_number_verified |
address | address |
UserInfo Endpoint
Endpoint: GET|POST /{issuer_id}/userinfo
Returns claims about the authenticated user. Requires a valid access token with the openid scope.
GET /userinfo
Authorization: Bearer eyJ...
Response:
{
"sub": "usr_xxx",
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true,
"picture": "https://example.com/photo.jpg"
}
Scopes
| Scope | Description |
|---|---|
openid | Required for OIDC flows; triggers ID token issuance |
profile | Basic profile information |
email | Email address and verification status |
phone | Phone number and verification status |
address | Physical address |
Nonce
The nonce parameter prevents replay attacks. When included in the authorization request, it’s embedded in the ID token for client verification.
GET /authorize?
...&
nonce=n-0S6_WzA2Mj
The client must verify that the nonce claim in the ID token matches the value sent in the request.
Authentication Context
ACR (Authentication Context Class Reference):
AuthPI returns ACR values indicating the authentication assurance level:
urn:authpi:assurance:loa1- Standard authenticationurn:authpi:method:{method}- Method-specific (e.g.,urn:authpi:method:passkey)
AMR (Authentication Methods References):
The amr claim contains an array of authentication methods used:
pwd- Passwordotp- One-time passwordmfa- Multi-factor authenticationhwk- Hardware key (passkey)
OpenID Connect Discovery 1.0
AuthPI publishes its configuration at the standard discovery endpoint.
Endpoint: GET /{issuer_id}/.well-known/openid-configuration
Response
{
"issuer": "https://idp.authpi.com/i_xxx",
"authorization_endpoint": "https://idp.authpi.com/i_xxx/authorize",
"token_endpoint": "https://idp.authpi.com/i_xxx/token",
"userinfo_endpoint": "https://idp.authpi.com/i_xxx/userinfo",
"jwks_uri": "https://idp.authpi.com/i_xxx/jwks.json",
"introspection_endpoint": "https://idp.authpi.com/i_xxx/introspect",
"revocation_endpoint": "https://idp.authpi.com/i_xxx/revoke",
"end_session_endpoint": "https://idp.authpi.com/i_xxx/logout",
"scopes_supported": ["openid", "email", "profile", "address", "phone"],
"response_types_supported": ["code"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:token-exchange"
],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["ES256", "RS256", "EdDSA"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"code_challenge_methods_supported": ["S256"],
"claims_supported": [
"sub", "iss", "aud", "exp", "iat",
"name", "given_name", "family_name",
"email", "email_verified",
"picture", "locale",
"acr", "amr", "auth_time"
],
"frontchannel_logout_supported": false,
"frontchannel_logout_session_supported": false,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true
}
When CIMD is enabled for the issuer, discovery also includes "client_id_metadata_document_supported": true. The property is omitted while CIMD is disabled.
JWKS Endpoint
Endpoint: GET /{issuer_id}/jwks.json
Returns the JSON Web Key Set containing public keys for verifying tokens.
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "key_xxx",
"alg": "ES256",
"crv": "P-256",
"x": "...",
"y": "..."
}
]
}
Keys are cached for 1 hour. Clients should cache JWKS responses and refresh periodically.
Session Management
AuthPI supports OIDC Session Management for detecting session changes.
Check Session Iframe
Endpoint: GET /{issuer_id}/check-session.html
Clients can embed this iframe and use postMessage to check if the user’s session is still valid without redirecting the browser or prompting the user to sign in.
Cross-site iframe checks use a separate HttpOnly; Secure; SameSite=None cookie limited to session checking. The main sign-in cookie remains SameSite=Lax. AuthPI issues or updates the companion at sign-in and authorization, and clears it alongside the sign-in cookie at logout. Existing sessions receive it on their next browser authorization request.
The browser must allow the IdP’s cookie in the embedded context. Third-party cookie blocking or storage partitioning can prevent iframe session checking even after a successful sign-in. Applications should handle an unavailable iframe and use a top-level authorization flow when they need to confirm the user’s session.
For native loopback callbacks that use a different port at sign-in, session checking permits only the exact origin recorded for that active client session. AuthPI binds it to the signed-in user’s issuer and SSO session; registering one loopback callback does not authorize every local port. CIMD session checks likewise use the active session’s actual origin, constrained by its saved client policy.
For an HTTP loopback page that embeds the iframe, use 127.0.0.1 or localhost. CSP frame-ancestor rules cannot permit literal IPv6 hosts, so [::1] remains supported for authorization but cannot embed this iframe. AuthPI does not widen the policy to all HTTP origins to work around this browser restriction.
RP-Initiated Logout
Endpoint: GET /{issuer_id}/logout
GET /logout?
id_token_hint=eyJ...&
post_logout_redirect_uri=https://app.example.com/logged-out&
state=xyz
| Parameter | Required | Description |
|---|---|---|
id_token_hint | Recommended | ID token for session identification |
post_logout_redirect_uri | Optional | Where to redirect after logout |
state | Optional | CSRF protection, echoed back |
The post_logout_redirect_uri must exactly match an entry in the registered client’s settings.openid.post_logout_redirect_uris, including its port. See post-logout redirect URIs.
When AuthPI can identify the underlying OP session, RP-initiated logout terminates that SSO anchor and the linked RP/client sessions created from it. Each affected client session can receive a back-channel logout notification when the client is configured for it.
Front-Channel Logout
AuthPI does not currently advertise or send OIDC front-channel logout iframe notifications. Use back-channel logout for client session notifications.
Back-Channel Logout
AuthPI can send logout tokens directly to relying parties’ back-channel logout endpoints. This is the supported logout notification mechanism and does not depend on the user’s browser.
Client Authentication
Supported Methods
| Method | Description |
|---|---|
client_secret_basic | Client ID and secret in HTTP Basic header |
client_secret_post | Client ID and secret in POST body |
none | No authentication (public clients only) |
Basic Authentication:
POST /token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=xxx&
redirect_uri=https://app.example.com/callback
POST Body Authentication:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=xxx&
redirect_uri=https://app.example.com/callback&
client_id=c_xxx&
client_secret=sec_xxx
Not Supported
private_key_jwt- JWT client assertion with private keyclient_secret_jwt- JWT client assertion with shared secrettls_client_auth- Mutual TLS authentication
Token Lifetimes
Default token lifetimes (configurable per client):
| Token Type | Default Lifetime |
|---|---|
| Authorization code | 10 minutes |
| Access token | 30 minutes |
| Refresh token | 7 days |
| ID token | Same as access token |
Error Responses
AuthPI returns OAuth 2.0 standard error responses:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}
Common error codes:
| Error | Description |
|---|---|
invalid_request | Missing or invalid parameter |
invalid_client | Client authentication failed |
invalid_grant | Authorization code or refresh token invalid |
unauthorized_client | Client not authorized for this grant type |
unsupported_grant_type | Grant type not supported |
invalid_scope | Requested scope is invalid or exceeds granted |
access_denied | User denied the authorization request |
Limitations
Features Not Implemented
- Hybrid flows:
response_typecombinations likecode id_tokenare not supported - Request objects: The
requestandrequest_uriparameters are not supported - Pushed Authorization Requests (RFC 9126): Not implemented
- DPoP (RFC 9449): Demonstration of Proof of Possession not supported
- CIBA: Client-Initiated Backchannel Authentication not supported
Parameter Limitations
promptparameter is recognized but not fully enforcedmax_ageparameter is not validatedui_localesparameter is not processedlogin_hintprefills the sign-in identifier. Users can edit it before submitting.screen_hint=signupopens registration when authentication is required. Existing sessions proceed normally, and the issuer’s signup policy still applies.
Security Recommendations
For Confidential Clients
- Store client secrets securely (environment variables, secret managers)
- Use HTTPS for all redirect URIs
- Implement PKCE even though it’s optional
- Rotate client secrets periodically
For Public Clients
- PKCE is mandatory—always use
S256 - Store tokens securely (avoid localStorage for sensitive data)
- Use short-lived access tokens
- Implement token refresh handling
For All Clients
- Validate ID token signatures using JWKS
- Verify
noncein ID tokens matches your request - Check
audclaim matches your client ID - Implement proper CSRF protection with
stateparameter - Use token introspection for high-security operations
Next Steps
- Getting Started with AuthPI
- Clients configuration guide
- Sessions and token management
- Webhooks for authentication events