Errors

TL;DR — Every error AuthPlane can return, in one place. OAuth errors follow RFC 6749 §5.2 with an RFC 9457 problem+json envelope on top. Auth challenges use RFC 6750 (Bearer) or RFC 9449 (DPoP) WWW-Authenticate headers. Consent errors from the SDKs surface as MCP JSON-RPC -32042 UrlElicitationRequiredError. Symptom→fix table at the bottom for the ones you’ll actually hit.

Error envelope

Every error response includes both OAuth error fields and RFC 9457 Problem Details fields, served with Content-Type: application/problem+json:

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "error": "invalid_grant",
  "error_description": "authorization code has already been used",
  "type": "https://docs.authplane.ai/errors/invalid_grant",
  "title": "Bad Request",
  "status": 400,
  "detail": "authorization code has already been used"
}
  • error + error_description — OAuth 2.1 wire format (RFC 6749 §5.2). Every OAuth client library reads these.
  • type + title + status + detail — RFC 9457 Problem Details. Standard HTTP APIs read these.

Both are always present. Pick whichever your stack understands.

OAuth error codes

Domain errors are sentinels in internal/domain/errors.go. Each carries an OAuth code + a default HTTP status.

OAuth error codes (mixed sources)

The codes below all use the RFC 6749 §5.2 envelope shape but come from different specs. Sourcing matters if you’re building on top of these: §5.2 defines only the six token-endpoint codes; the rest are borrowed from the authorization endpoint, other RFCs, or OIDC.

OAuth codeHTTPSourceTypical cause
invalid_request400RFC 6749 §5.2Missing/malformed parameter, bad redirect_uri
invalid_client401RFC 6749 §5.2Unknown client_id, wrong secret, suspended client
invalid_grant400RFC 6749 §5.2Expired code, wrong PKCE verifier, replayed code, revoked refresh family
unauthorized_client400RFC 6749 §5.2Client lacks the requested grant_type in its registration
unsupported_grant_type400RFC 6749 §5.2Grant type disabled at the AS (e.g. client_credentials.enabled: false)
invalid_scope400RFC 6749 §5.2Scope not declared on the target resource
access_denied403RFC 6749 §4.1.2.1 (authorize endpoint)User denied consent; DCR blocked; state-token owner mismatch
server_error500RFC 6749 §4.1.2.1 (authorize endpoint)Encryption/decryption failed, key not found, rotation conflict
consent_required400OIDC Core §3.1.2.6Token exchange target needs upstream user consent (Broker resource)
slow_down429RFC 8628 (device flow)Rate limit hit

DPoP-specific codes (RFC 9449)

RFC 9449 defines two different delivery mechanisms depending on which server rejects the proof:

  • Authorization server (/oauth/token, §8) — HTTP 400 with the RFC 6749 §5.2 JSON envelope {"error":"…"}. No WWW-Authenticate header. If the AS needs a nonce it also sets DPoP-Nonce: <value>.
  • Resource server (§9) — HTTP 401 with WWW-Authenticate: DPoP error="…". This is the challenge form clients see on tools/* calls.
CodeAS (§8)RS (§9)Meaning
invalid_dpop_proof400401Proof malformed, wrong htm/htu/ath, replayed jti, signature invalid, JWK thumbprint doesn’t match cnf.jkt, or two DPoP headers on the same request (§4.3)
use_dpop_nonce400401Server requires nonce; response includes DPoP-Nonce: <value>

Fix for either: regenerate the proof against the exact request URL + method, and (if the server sent DPoP-Nonce) include that value as the nonce claim.

AuthPlane-specific codes

CodeHTTPMeaning
scope_not_granted400Broker resource: requested scope not in the user’s stored broker_grants.scopes_granted — need re-consent via /connect/{provider}
token_expired400Vault: upstream access token expired and cannot be refreshed (no refresh token stored)
not_found404Admin API: resource / user / client / grant doesn’t exist
conflict409Admin API: unique constraint (duplicate slug on create, concurrent update version mismatch)

WWW-Authenticate challenge patterns

The WWW-Authenticate response header tells clients how to retry.

Bearer (RFC 6750 §3)

WWW-Authenticate: Bearer realm="https://mcp.example.com/mcp",
                  error="invalid_token",
                  error_description="The access token expired",
                  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

The resource_metadata field (RFC 9728 §5.1) tells the client where to fetch the PRM document — which in turn tells it where to fetch a new token. All AuthPlane SDKs emit this automatically on 401 responses.

DPoP (RFC 9449 §7.1)

WWW-Authenticate: DPoP realm="https://mcp.example.com/mcp",
                  error="invalid_dpop_proof",
                  error_description="DPoP proof jti has already been used",
                  algs="ES256 RS256 PS256",
                  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

For use_dpop_nonce the response ALSO includes:

DPoP-Nonce: <server-generated-nonce-value>

The client must include this nonce in the next DPoP proof’s nonce claim.

Insufficient scope

Whether Bearer or DPoP, scope failures return error="insufficient_scope" with a scope= field listing what’s required:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                  error_description="token missing required scope",
                  scope="tools/write"

MCP JSON-RPC errors

Two MCP-specific errors that the SDKs surface on top of the OAuth layer:

-32042 — URL elicitation required

Emitted when a tool handler triggers a client.exchange() call that hits consent_required on a Broker resource. The SDK auto-translates the OAuth error into an MCP JSON-RPC error -32042 with a consentUrl in the data payload:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32042,
    "message": "URL elicitation required",
    "data": {
      "elicitation_id": "elic_...",
      "url": "https://auth.example.com/connect/github?return_to=..."
    }
  }
}

The MCP client shows the user the URL, they complete the Connect flow, then the client retries the original tools/call.

The SDKs handle this automatically — you don’t try/except for it in tool code. See Concepts: Token Vault.

Complete domain error catalog

Every error sentinel in internal/domain/errors.go. Grouped by concern.

OAuth core

SentinelCodeTypical trigger
ErrInvalidGrantinvalid_grantExpired/wrong code, wrong verifier, wrong redirect_uri
ErrInvalidClientinvalid_clientUnknown client_id, wrong secret
ErrClientNotFoundnot_foundAdmin lookup miss (distinct from OAuth invalid_client)
ErrInvalidScopeinvalid_scopeScope not in client’s allowed set
ErrCodeConsumedinvalid_grantAuth code replay (already used)
ErrFamilyRevokedinvalid_grantRefresh token family revoked (theft detected)
ErrConsentRequiredconsent_requiredUser consent needed but not present
ErrSessionExpiredinvalid_grantAuth session timed out
ErrInvalidRedirectURIinvalid_requestredirect_uri doesn’t match registration
ErrInvalidPKCEinvalid_grantPKCE verification failed
ErrClientSuspendedinvalid_clientClient suspended or revoked
ErrRateLimitedslow_downToo many requests
ErrUserNotFoundnot_foundUnknown user
ErrInvalidCredentialsinvalid_grantWrong password
ErrUnsupportedGrantTypeunsupported_grant_typeGrant disabled at AS
ErrRefreshTokenReusedinvalid_grantRefresh token already consumed
ErrRegistrationDisabledaccess_deniedDCR mode is admin_only
ErrUnauthorizedClientunauthorized_clientClient not permitted for grant type

CIMD

SentinelCodeTrigger
ErrCIMDFetchFailedinvalid_clientHTTP fetch of CIMD doc failed
ErrCIMDInvalidinvalid_clientCIMD doc validation failed

OIDC federation

SentinelCodeTrigger
ErrOIDCAuthFailedaccess_deniedUpstream OIDC authentication failed

Encryption + keys

SentinelCodeTrigger
ErrEncryptionFailedserver_errorEncryption failed (bad input)
ErrDecryptionFailedserver_errorDecryption failed (wrong key/context/tampered)
ErrEncryptorUnavailableserver_errorEncryption backend down
ErrRotationConflictserver_errorConcurrent key rotation detected
ErrKeyNotFoundserver_errorSigning key not found in store

Broker / Vault

SentinelCodeTrigger
ErrConnectionNotFoundnot_foundVault connection not found
ErrConnectionConflictconflictConcurrent update detected
ErrConnectionExistsconflictConnection already exists for (owner, service)
ErrStateNotFoundinvalid_requestState token expired or consumed
ErrServiceNotFoundinvalid_requestConnector service not registered
ErrInvalidStateinvalid_requestState token tampered
ErrStateForeignUseraccess_deniedState token belongs to a different user
ErrInvalidReturnURLinvalid_requestReturn URL not in allowed_return_urls
ErrScopeNotGrantedscope_not_grantedRequested scope not in broker_grants.scopes_granted
ErrVaultTokenExpiredtoken_expiredUpstream token expired and no refresh available

DPoP

SentinelCodeTrigger
ErrDPoPReplayinvalid_dpop_proofProof jti already used
ErrDPoPInvalidProofinvalid_dpop_proofBad alg/htm/htu/iat/ath/signature
ErrDPoPNonceRequireduse_dpop_nonceServer nonce required but missing

Symptom → cause → fix (top 20)

The ones you’ll actually hit while wiring things up.

SymptomCauseFix
401 invalid_token on every request, no other detailWrong aud — token issued for a different resource URI than yoursEnsure the client sends resource=<your-uri> on /authorize and /token. The URI must match the resource registration byte-for-byte
401 invalid_dpop_proof when client IS sending a DPoP headerPython authplane-mcp: forgot install_request_context(mcp); any language: htu mismatch because a reverse proxy rewrote scheme/hostCall install_request_context(mcp) in Python; audit X-Forwarded-Proto/Host on the proxy
401 invalid_dpop_proof on second request from same clientJTI replay — proof jti reused (bug in client)Client must generate a fresh jti per request
401 use_dpop_nonce on first DPoP requestServer requires nonce (dpop.require_nonce: true)Client picks up DPoP-Nonce response header, includes it in next proof’s nonce claim
403 insufficient_scope on a specific toolToken’s scope claim doesn’t contain what require_scope demandsCheck granted scopes in the Admin UI Issuances panel; may need to expand scopes on the resource registration or the client’s scope field
400 invalid_grant "authorization code has already been used"Client tried to exchange the same code twice, or the code expired (AuthCodeTTL = 10 minutes)Restart the flow; codes are single-use
400 invalid_grant "PKCE verification failed"Client sent wrong code_verifier or generated the wrong code_challengeEnsure code_challenge = BASE64URL(SHA256(code_verifier))
400 invalid_grant "refresh token has already been used" OR "token family revoked due to reuse detection"Refresh token was used twice — either concurrent refresh or theftRestart auth from scratch. All tokens in the family are now revoked
400 unsupported_grant_type on client_credentialsclient_credentials.enabled: false (default)Set env AUTHPLANE_CLIENT_CREDENTIALS_ENABLED=true and restart
400 unsupported_grant_type on token-exchange or jwt-bearerSame — disabled by defaultSet AUTHPLANE_TOKEN_EXCHANGE_ENABLED=true or xaa.enabled: true
400 unauthorized_clientClient’s grant_types doesn’t include the requested grantUpdate the client via authserver admin client update
400 invalid_scopeRequested scope not registered on the target resourceAdd the scope to the resource, or drop it from the request
400 consent_required + consent_url in the token-exchange responseUser hasn’t completed the Connect flow for a Broker resourceRedirect user to the consent_url (SDK does this automatically via MCP -32042)
MCP client shows -32042 URL elicitation requiredSame as above — token-exchange consent neededFollow the URL in error.data.url
PRM 404 at /.well-known/oauth-protected-resourceSDK didn’t mount the PRM handler (Go only; Python/TS auto-mount)http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler())
Discovery 404 at /.well-known/oauth-authorization-serverWrong issuer URL — MCP client is hitting the resource server instead of the ASCheck that PRM’s authorization_servers field points at AuthPlane, not your MCP server
Boot fails with “session.secret is required”server.issuer is not localhost and session.secret is unsetopenssl rand -hex 32 and set AUTHPLANE_SESSION_SECRET
Boot fails with “admin.api_key is required”Same — production issuer without an API keyGenerate one and set AUTHPLANE_ADMIN_API_KEY
Admin UI shows “Failed to fetch” everywhereCORS not configured; browser blocks calls to :9001 from a different originSet AUTHPLANE_SERVER_ALLOWED_ORIGINS with the origin of your admin UI host
MCP Inspector says “no tools”Inspector URL points at the host without /mcpUse npx @modelcontextprotocol/inspector http://localhost:8080/mcp

Reading structured logs

Every error path logs a structured slog event at WARN or ERROR with:

  • error — the sentinel name
  • code — the OAuth error code
  • client_id, resource, grant_type — request context when available
  • trace_id, span_id, request_id — for correlation

Example (auth code replay):

2026-07-01T00:14:20Z ERROR msg="token exchange failed"
  error=ErrCodeConsumed code=invalid_grant
  client_id=my-client resource=https://mcp.example.com/mcp
  request_id=r_abc123 trace_id=t_def456

Grep or ship to a log aggregator; every error field is queryable.