Skip to content

Error catalog

The OP returns errors from a closed catalog. There is no free-form fmt.Errorf reaching the wire — every emitted error either comes from op.Error (configuration / construction) or from one of the endpoint-internal code constants (request handling).

This page enumerates both.

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 inside an endpointwhile a client is waitingA closed catalogerror + error_descriptionthe code is the contractJSON responseback-channel endpointsRedirectonce redirect_uri is trustedHTML error pagewhen it is not, or not yet
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 (/authorize, /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 (e.g. multiple configuration_error sentinels) without being interchangeable.

Construction-time errors (op.New)

Returned by the constructor; the OP never starts.

SentinelCodeTrigger
op.ErrIssuerRequiredconfiguration_errorWithIssuer not supplied
op.ErrIssuerInvalidconfiguration_errorissuer is not absolute https, or has query / fragment
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.ErrDynamicRegistrationDisabledconfiguration_errorProvider.IssueInitialAccessToken called without WithDynamicRegistration

Other config errors

Profile / option conflicts (e.g. 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 — match on op.IsServerError or inspect .Code.

Authorize endpoint (/authorize)

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
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
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

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
use_dpop_nonce400DPoP §8 server nonce required; client must retry with DPoP-Nonce
server_error500internal failure

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.

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 is preserved; 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

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

/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 op.AuditIntrospectionError; 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.

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

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, 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.

Class-based dispatch

Use the predicates instead of switching on each code:

go
switch {
case errors.Is(err, op.ErrIssuerRequired):
    // boot-time fix needed
case op.IsClientError(err):
    // 4xx — log at info, don't alert
case op.IsServerError(err):
    // 5xx — alert on rate
}

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.