Skip to content

Error catalog ​

The OP returns errors from a closed catalog. No free-form fmt.Errorf reaches the wire: every error value comes from op.Error (configuration and construction) or from one of the endpoint-internal code constants (request handling). This page lists both, grouped by endpoint.

Two kinds of error, and where each one surfaces
Configuration errors are returned by op.New and stop the process from starting. Request errors come out of an endpoint and are rendered as a JSON body, a redirect carrying error parameters, or an HTML error page, depending on where the request was in the flow.Construction errorreturned by op.Newthe process never startsRequest errorraised in an endpointwhile a client waitsA closed catalogerror + error_descriptionthe code is the contractJSON responseback-channel endpointsRedirectto a verified redirect_uriHTML error pageredirect_uri unverified
Which of the three a client sees is not a style choice. Until redirect_uri has been validated the OP cannot redirect anywhere — sending an error to an unverified URI is how open redirectors get built.

Error shape ​

Every wire-emitted error follows OAuth 2.0 / OIDC conventions:

json
{
  "error": "invalid_request",
  "error_description": "client_id is required"
}
  • error is one of the codes below — machine-readable, never localised, never wrapped.
  • error_description is a short hint for operators; it never contains tokens, raw input, or stack traces.

For browser-facing endpoints (/auth, /end_session) the same codes appear in:

  • The error query / fragment parameter on the redirect (when redirect_uri and state could be validated).
  • The HTML error page DOM (<div id="op-error" data-code="..." data-description="...">) when no safe redirect exists.

Programmatic discrimination ​

go
import "errors"

if errors.Is(err, op.ErrIssuerRequired) { /* config-time */ }

if op.IsClientError(err) { /* 4xx-class */ }
if op.IsServerError(err) { /* 5xx-class */ }

Sentinel comparison uses pointer identity (Go's default errors.Is). Two distinct sentinels can share an OAuth code (several are configuration_error) without being interchangeable.

The predicates test the Code of an *op.Error, so they cover exactly these codes:

PredicateCodes it matches
op.IsClientErrorinvalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, unsupported_response_type, invalid_scope, access_denied
op.IsServerErrorserver_error, temporarily_unavailable, configuration_error

A wire code outside both lists, such as use_dpop_nonce, is not an *op.Error the embedder receives from op.New; it only appears in HTTP responses.

Construction-time errors (op.New) ​

Returned by the constructor; the OP never starts.

SentinelCodeTrigger
op.ErrIssuerRequiredconfiguration_errorWithIssuer not supplied
op.ErrIssuerInvalidconfiguration_errorissuer is not an absolute URL in canonical form: it has a trailing slash, query or fragment, or its scheme is not https (http is admitted only for a loopback IP literal, and for localhost with WithAllowLocalhostIssuerForDev)
op.ErrStoreRequiredconfiguration_errorWithStore not supplied
op.ErrUserStoreRequiredconfiguration_errorWithUserStore received nil
op.ErrKeysetRequiredconfiguration_errorWithKeyset not supplied or empty
op.ErrCookieKeysRequiredconfiguration_errorWithCookieKeys missing while authorization_code grant is enabled
op.ErrLoginFlowRequiredconfiguration_errorWithLoginFlow missing while authorization_code grant is enabled
op.ErrAuthorizationCodeGrantRequiredconfiguration_errorWithLoginFlow, WithUI, WithInteractions or WithFirstPartyClients set explicitly while WithGrants leaves out grant.AuthorizationCode (options inside WithDefaults are not counted)
op.ErrAuthnLockoutStoreRequiredconfiguration_errorthe login flow has a step that counts failures (password, second factor, or an op.ExternalStep with CountFailures) and no lockout store resolves from WithStore or WithAuthnLockoutStore
op.ErrSubjectModeMismatchconfiguration_errorthe subject strategy (public, pairwise, custom generator) differs from the marker recorded against a populated grant store; provision a new issuer instead of switching
op.ErrDynamicRegistrationDisabledconfiguration_errorProvider.IssueInitialAccessToken called without WithDynamicRegistration
op.ErrPairwiseSaltTooShortconfiguration_errorWithPairwiseSubject salt shorter than 32 bytes
op.ErrSubjectGeneratorRequiredconfiguration_errorWithSubjectGenerator received nil
op.ErrCustomGrantNilconfiguration_errorWithCustomGrant received a nil handler
op.ErrCustomGrantNameEmptyconfiguration_errorCustomGrantHandler.Name returned an empty grant_type URN
op.ErrCustomGrantBuiltinCollisionconfiguration_errorhandler name equals a built-in grant_type
op.ErrCustomGrantDuplicateconfiguration_errorhandler name already registered by an earlier WithCustomGrant
op.ErrCustomGrantSecretLikeExemptconfiguration_errorParamPolicy.DupesAllowed names a security-sensitive parameter
op.ErrTokenExchangePolicyNilconfiguration_errorWithTokenExchangeGrant received a nil policy
op.ErrTokenExchangeDuplicateconfiguration_errorWithTokenExchangeGrant called twice

Four more sentinels are returned after construction rather than by op.New:

SentinelCodeTrigger
op.ErrReloadIncompatibleconfiguration_errorReloadable.Reload given options that cannot replace the running ones (a changed issuer or route set, a dropped active cookie, signing or MFA key, a different DPoP nonce source); the returned error wraps it, so match with errors.Is and restart the process
op.ErrLocaleNotRegisteredinvalid_requestProvider.SetLocaleCookie given a locale no registered bundle matches
op.ErrSubjectInputEmptyserver_errora built-in subject generator called with an empty internal user ID
op.ErrPairwiseSectorUnresolvedserver_errorpairwise subject for a client with no sector_identifier_uri and not exactly one redirect_uri host

Sentinels your code returns ​

These are not produced by the OP. An embedder's callback returns one to choose the wire answer; each is matched with errors.Is.

SentinelReturned fromWire effect
op.ErrFactorRetryan op.Authenticator (op.ExternalStep) Begin / Continue, wrappedwrong credential: counted as a failure and the same prompt is shown again
op.ErrFactorAbortan op.Authenticator, wrappedends the attempt with a 4xx for a failure the user caused
op.ErrUnknownCIBAUserHintResolver.Resolve/bc-authorize answers unknown_user_id; any other resolver error answers login_required
op.ErrUnknownAssertionSubjectJWTBearerGrant.ResolveSubject/token answers access_denied; any other error, or an empty subject, answers server_error
op.ErrIDJAGUnknownAudienceIDJAGIssuancePolicy.Allow (experimental)/token answers invalid_target
op.ErrIDJAGDeniedIDJAGIssuancePolicy.Allow (experimental)/token answers access_denied

The op/store package defines the sentinels a store implementation returns so handlers can use errors.Is across backends: store.ErrNotFound (a missing record; never a backend-specific "no rows"), store.ErrAlreadyExists (a duplicate primary or unique key), store.ErrAlreadyConsumed (a single-use record that was already used, or a rotation onto a retired chain), store.ErrConflict (a lost compare-and-swap; the caller may retry), and store.ErrTxRequired (a closed transaction handle, or an operation a backend exposes only inside one). store.ErrPasswordHashUpdateUnsupported is returned by a UserPasswordHashUpdater that cannot persist a new hash for a subject; the OP then keeps the legacy hash and logs once.

Other config errors

Profile and option conflicts (for example WithProfile(FAPI2Baseline) together with WithAccessTokenFormat(Opaque)) return an *op.Error with Code = configuration_error and a description naming the conflicting options. They are not pre-allocated sentinels, so match with op.IsServerError or inspect .Code. The options reference lists the refusals.

Authorize endpoint (/auth) ​

Browser-facing. Reach the user via redirect when redirect_uri could be validated, otherwise via the HTML error page.

CodeSpecTypical cause
invalid_requestOIDC Core §3.1.2.6missing parameter, malformed PKCE, state exceeds size cap
invalid_request_objectRFC 9101 §6.1JAR JWS fails verification, alg not allowed, expired
invalid_request_uriRFC 9101 §6.1 / RFC 9126 §2.2PAR request_uri already consumed / expired
invalid_scopeOIDC Core §3.1.2.6requested scope not in catalog
invalid_targetRFC 8707 §2resource malformed, duplicated, or not an allowed resource
invalid_authorization_detailsRFC 9396 §5authorization_details fails type validation
unmet_authentication_requirementsOIDC Core §5.5.1.1, RFC 9470 §4the session cannot satisfy an acr the request marked essential
unsupported_response_typeOIDC Core §3.1.2.6response_type not code (Implicit / Hybrid refused)
unsupported_response_modeOAuth 2.1unsupported response_mode
login_requiredOIDC Core §3.1.2.6prompt=none and no active session
consent_requiredOIDC Core §3.1.2.6prompt=none but consent missing
interaction_requiredOIDC Core §3.1.2.6prompt=none and the flow needs UI, including a LoginFlow rule or Decider that demands another step on an existing session
account_selection_requiredOIDC Core §3.1.2.6prompt=none with multiple sessions
access_deniedOIDC Core §3.1.2.6user canceled at consent / login
server_errorOIDC Core §3.1.2.6internal failure, including a client-store fault (the OP does not report the client_id as unregistered)

A request object whose client JWKS cannot be fetched because the fetch is overloaded or failing transiently is answered with HTTP 503 server_error on the error page. A consent or chooser submission that is declined, stale or tampered, or names an expired or foreign chooser session_id, is a 4xx answer; a chooser-selected session that has expired or signed out is reported as login_required.

Redirect URI mismatch is special

invalid_request: redirect_uri does not match a registered URI is the only authorize-time error the OP refuses to redirect with — the URL is by definition untrusted. The user reaches the HTML error page. See FAQ § Common errors.

Token endpoint (/token) ​

JSON. RFC 6749 §5.2 + RFC 9449 (DPoP).

CodeHTTPTrigger
invalid_request400malformed grant body, missing parameter
invalid_grant400code expired / consumed, refresh token rotated past grace, PKCE verifier mismatch
invalid_client401client auth failed (no credentials, wrong secret, invalid private_key_jwt)
unauthorized_client400client not allowed for this grant type
unsupported_grant_type400grant not enabled in WithGrants
invalid_scope400refresh requested wider scope than original
invalid_target400RFC 8707 resource / audience refused by policy
invalid_authorization_details400authorization_details not permitted for this request
authorization_pending400device-code or CIBA poll before the user has decided
slow_down400device-code or CIBA poll faster than the negotiated interval
expired_token400device_code or auth_req_id lifetime elapsed
access_denied400the user denied the device-code or CIBA request
use_dpop_nonce400DPoP §8 server nonce required; client must retry with DPoP-Nonce
server_error500internal failure, including a client-store outage during client authentication
server_error503the client's JWKS could not be fetched (fetch overloaded or failing transiently); retryable

Client authentication failures map as follows on /token, /par, /device_authorization, /bc-authorize, /introspect and /revoke:

ConditionResponse
no credentials401 invalid_client
wrong secret, bad or replayed assertion (including an empty or over-long jti), client mismatch401 invalid_client
more than one credential method, or an unsupported method400 invalid_request
client JWKS fetch overloaded or failing503 server_error
any other fault, such as a client-store outage500 server_error

401 invalid_client carries WWW-Authenticate: Basic realm="oidc" — the realm is a fixed literal, not the issuer URL — when the client authenticated via HTTP Basic; /userinfo's own 401s use a separate Bearer realm="userinfo" challenge, per RFC 6749 §5.2.

A refresh request that lists a narrower scope does not narrow the rotated refresh token: the successor keeps the scope of the token it replaces. An authorization code presented by the wrong client, or one that fails the redirect_uri or PKCE check, answers invalid_grant without consuming the code; only a genuine replay revokes the grant.

The urn:ietf:params:oauth:grant-type:jwt-bearer grant answers a public client or a client outside the issuer's AllowedClients with unauthorized_client, a defective assertion (unknown issuer, bad typ, signature, claims, replay) with invalid_grant, a scope outside the client's registered scopes (or openid) with invalid_scope, and a valid assertion whose subject has no provisioned account with access_denied. An issuer key source that is unavailable answers 503 server_error; a resolver fault answers 500 server_error. The assertion-level reason is in audit.JWTBearerRejected.

Custom-grant dispatch keeps client-attributable failures in the 4xx catalog: an unsupported grant, a client grant-type restriction, malformed or duplicate parameters, and scope or audience policy failures map to unsupported_grant_type, unauthorized_client, invalid_request, invalid_scope, or invalid_target. An un-attributed dispatcher or OP issuance failure — including a handler panic, conflicting access-token forms, reserved-claim collisions, invalid token data, or a backing-store failure — returns 500 server_error. A handler-provided *op.Error, including a wrapped one, keeps its code on the wire; another handler error defaults to 400 invalid_grant.

UserInfo endpoint (/userinfo) ​

Bearer-token validation. RFC 6750 §3.1 + RFC 9449.

CodeHTTPTrigger
invalid_token401bearer missing, expired, revoked, alg mismatch
invalid_dpop_proof401DPoP proof JWS invalid / replay / cnf.jkt mismatch
use_dpop_nonce401DPoP §8 server-nonce required
invalid_request400malformed bearer request (multiple tokens, wrong channel); 413 when the request body exceeds the size cap

A grant-store error other than store.ErrNotFound returns plain HTTP 500 without releasing claims. ErrNotFound means no grant lineage is available: without a subject projector, claim resolution falls back to the token's scopes; with pairwise subject projection, the request returns 401 invalid_token because the internal subject cannot be resolved.

Introspection / Revocation (/introspect, /revoke) ​

RFC 7662 / RFC 7009. Same client-auth code set as /token.

CodeHTTPTrigger
invalid_request400missing token parameter
invalid_client401client auth failed
server_error500internal failure
server_error503the client's JWKS could not be fetched for private_key_jwt authentication

/revoke supports both RFC 7009 token types, access_token and refresh_token, and never emits unsupported_token_type. An unknown or absent token_type_hint is handled as a search hint rather than a wire error; an unrecognized token still follows RFC 7009's idempotent HTTP 200 success contract.

Failed client authentication at /introspect is also visible as audit.IntrospectionError; the wire response remains the generic invalid_client. A non-NotFound lookup fault in the opaque access-token or refresh-token store is audited with the same event. It still returns HTTP 200 with {"active":false} rather than server_error. A normal inactive miss emits no fault event. /revoke follows the RFC 7009 response contract and does not use that introspection-specific audit name.

/revoke answers 200 even when the token's rotation chain is longer than the chain-root lookup can walk; it then revokes the grant's refresh tokens instead, and token.revoked reports surface: "refresh_grant" (Audit event catalog).

For a refresh token, /introspect answers active with the refresh-token state but no token_type: RFC 7662 defines that member as the access-token type, so a resource server cannot mistake a refresh token for a bearer token.

PAR endpoint (/par) ​

RFC 9126.

CodeHTTPTrigger
invalid_request400malformed authorize parameters in PAR body
invalid_request_object400JAR signature fails (when request parameter present)
invalid_client401client auth failed
server_error503the client's JWKS could not be fetched (client authentication or request-object verification)

Device authorization (/device_authorization) and CIBA (/bc-authorize) ​

RFC 8628 and CIBA Core §13. Both use the RFC 6749 §5.2 envelope; the polling codes (authorization_pending, slow_down, expired_token, access_denied) are returned by /token, listed above.

CodeHTTPEndpointTrigger
invalid_request400bothmalformed or missing parameter, or malformed client certificate
unauthorized_client400bothclient is not registered for the grant
invalid_scope400bothscope refused
invalid_target400bothresource refused
unknown_user_id400/bc-authorizethe hint resolver found no user
invalid_binding_message400/bc-authorizebinding_message rejected
login_required400/bc-authorizethe hint resolver failed or returned an empty subject
server_error500 / 503bothinternal failure; 503 when a request object's client JWKS is unavailable

Dynamic Client Registration (/register, /register/{client_id}) ​

RFC 7591 / RFC 7592.

CodeHTTPTrigger
invalid_request400missing required metadata
invalid_token401IAT / RAT bearer missing / expired
invalid_client_metadata400metadata fails policy (e.g. unsupported response_types)
invalid_redirect_uri400redirect URI shape rejected (loopback wildcard, fragment, …)
invalid_software_statement400software statement JWS fails verification
server_error500internal failure

End-session (/end_session) ​

OIDC RP-Initiated Logout 1.0. Browser-facing; reaches the user via post_logout_redirect_uri redirect or the HTML error page.

CodeTrigger
invalid_requestmalformed id_token_hint, an id_token_hint whose typ header is anything other than absent or JWT (a logout token or an access token is refused), mismatched client_id, or invalid logout_scope (empty, unknown, repeated, or not exactly current)
invalid_request_urirequest_uri (when JAR-style logout requests are used) expired

If the session store cannot be reached while /end_session resolves or destroys the session, the endpoint returns a static HTTP 503 error page. It does not redirect to post_logout_redirect_uri, and it preserves the browser cookie and session row so a transient store fault cannot turn into a partial logout. A missing or already-expired session is handled separately and is not the transport-failure case.

Request bodies ​

Every endpoint and driver that reads a JSON body rejects data after the first JSON value (a stray }, for example) with invalid_request, instead of ignoring the trailing bytes.

Verifying this list ​

sh
git clone https://github.com/libraz/go-oidc-provider.git
cd go-oidc-provider
grep -rhE '"[a-z_]+_[a-z_]+"' internal/*/error*.go \
  | grep -oE '"[a-z_]+_[a-z_]+"' | sort -u

The output is the union of every wire code the endpoints emit; this page groups them by endpoint with the spec citation for each.