Common errors

TL;DR — The errors you’ll actually hit while wiring things up, in the order you’ll hit them. Each has cause + fix. Full domain-error catalog + OAuth error codes + WWW-Authenticate patterns live in Reference: Errors — this page is the operator-focused subset.

Authentication errors

401 invalid_token on every request, no other detail

Cause: wrong aud — token issued for a different resource URI than yours. Fix: ensure the client sends resource=<your-uri> on /oauth/authorize and /oauth/token. The URI must match the resource registration byte-for-byte. See Configuration → Resources for the URI-mismatch trap.

401 invalid_dpop_proof when the client IS sending a DPoP header

Cause A (Python): forgot install_request_context(mcp) — the adapter can’t build the DPoP request context without it, so every proof fails closed. Cause B (any language): reverse proxy rewrote the scheme or host. The server derives htu from r.Host and only honors X-Forwarded-Proto for the scheme (nothing for host) — so if the proxy is presenting a different Host to AuthPlane than the client is signing over, DPoP fails closed. Fix A: add the install_request_context(mcp) call after mcp = FastMCP(...). Fix B: configure the proxy to preserve the original Host header (nginx: proxy_set_header Host $host; — the default forwards $proxy_host, which is wrong). If you’re terminating TLS at the proxy, also set X-Forwarded-Proto: https. See Guides: Enable DPoP → reverse-proxy trap.

401 invalid_dpop_proof "jti already used" on the second request from the same client

Cause: client is reusing the same DPoP proof jti across requests. Fix: client bug — generate a fresh UUID per proof. Every AuthPlane SDK does this correctly; if you’re implementing a client yourself, this is the first thing to check.

401 use_dpop_nonce on the first DPoP request

Cause: AS requires nonce (dpop.require_nonce: true); client didn’t send one. Fix: client extracts the DPoP-Nonce header from the 401 response and includes it in the next proof’s nonce claim. All AuthPlane SDKs handle this automatically.

403 insufficient_scope on a specific tool

Cause: the token’s scope claim doesn’t contain what require_scope demands. Fix: check granted scopes in the Admin UI Issuances panel. May need to expand scopes on the resource registration, or the client’s scope field on registration.

OAuth flow errors

400 invalid_grant "authorization code has already been used"

Cause: client tried to exchange the same code twice, or the code expired (AuthCodeTTL = 10 minutes — codes are single-use). Fix: restart the flow — codes are single-use. If you’re seeing this constantly, check whether your client is retrying after network errors (retry logic should NOT re-use the same code).

400 invalid_grant "PKCE verification failed"

Cause: client sent wrong code_verifier or generated the wrong code_challenge. Fix: ensure code_challenge = BASE64URL(SHA256(code_verifier)). Common bug: URL encoding the verifier before hashing, or hashing the wrong string.

400 invalid_grant "refresh token has already been used" OR "token family revoked due to reuse detection"

Cause: refresh token used twice — either concurrent refresh (client bug) or theft. AuthPlane treats every refresh-token reuse as theft and revokes the entire token family. Fix: restart auth from scratch. All tokens in the family are revoked. If you keep hitting this, your client has a concurrency bug — serialize refresh calls per token family.

400 unsupported_grant_type on client_credentials

Cause: client_credentials.enabled: false (the default). Fix: set AUTHPLANE_CLIENT_CREDENTIALS_ENABLED=true and restart.

400 unsupported_grant_type on token-exchange or jwt-bearer

Cause: same — grants are disabled by default. Fix: AUTHPLANE_TOKEN_EXCHANGE_ENABLED=true or xaa.enabled: true (YAML only).

400 unauthorized_client

Cause: client’s registration grant_types doesn’t include the requested grant. Fix: update the client via authserver admin client update or re-register.

400 invalid_scope

Cause: requested scope not registered on the target resource. Fix: add the scope to the resource, or drop it from the request. Scopes must be declared at the resource level, not just on the client.

Broker / upstream errors

Cause: user hasn’t completed the Connect flow for a Broker resource (consent_url will point at /connect/{provider}), OR requested scopes exceed what the user consented to (consent_url will point at re-consent). Fix: MCP SDK auto-translates this to MCP JSON-RPC -32042; MCP client shows the URL to the user. If you’re seeing raw consent_required, your SDK’s elicitation wiring is broken — check the SDK’s URL elicitation setup.

MCP client shows -32042 URL elicitation required

Cause: same as above — token-exchange consent needed. Fix: follow the URL in error.data.url (Connect flow or re-consent). SDK / client should do this automatically.

Configuration & boot errors

Boot fails with "session.secret is required"

Cause: server.issuer is not localhost and session.secret is unset. Fix: openssl rand -hex 32 and set AUTHPLANE_SESSION_SECRET.

Boot fails with "admin.api_key is required"

Cause: same — production issuer without an API key. Fix: generate one and set AUTHPLANE_ADMIN_API_KEY.

Boot fails with "data_encryption required when broker_providers configured"

Cause: you registered broker providers but didn’t enable encryption at rest. Fix: set data_encryption.driver to aes_master (simplest) or vault_transit_encrypt (enterprise). See Guides: Wire up the Token Vault → Step 1.

Admin UI shows “Failed to fetch” everywhere

Cause: CORS not configured; browser blocks calls to :9001 from a different origin. Fix: set AUTHPLANE_SERVER_ALLOWED_ORIGINS with the origin of your admin UI host.

Discovery errors

PRM 404 at /.well-known/oauth-protected-resource

Cause: SDK didn’t mount the PRM handler (Go only; Python/TS auto-mount). Fix (Go): http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler()).

AS metadata 404 at /.well-known/oauth-authorization-server

Cause: MCP client is hitting the wrong URL — probably your resource server instead of AuthPlane. Fix: check that PRM’s authorization_servers field points at AuthPlane, not your MCP server’s URL.

MCP Inspector says “no tools”

Cause: Inspector URL points at the host root without /mcp. Fix: use npx @modelcontextprotocol/inspector http://localhost:8080/mcp — with the /mcp path or whatever your SDK’s endpoint is configured for.

When you can’t find it here