Repository navigation
Errors and Observability
Every refusal is an *auth.Error and every response the library writes is {"code","message"} with Cache-Control: no-store. Components never log; they report one auth.Outcome per action to an optional auth.Observer.
&auth.Error{Kind, Code, Reason, Err}: Kind is one of the four sentinels and decides status and message; Code replaces the default code (stable, for frontends); Reason and Err stay server-side (wide event, outcome), never in the body. Branch with errors.Is(err, auth.ErrForbidden); the kind survives %w wrapping.
| Kind | Status | Default code | Message |
|---|---|---|---|
auth.ErrUnauthenticated |
401 | unauthenticated |
authentication required |
auth.ErrForbidden |
403 | forbidden |
access denied |
auth.ErrInvalidRequest |
400 | invalid_request |
invalid request |
auth.ErrConflict |
409 | conflict |
conflict |
| anything else | 500 | internal |
internal error |
auth.ResponseFor(err) returns status and body (the outermost *auth.Error decides); auth.WriteError(writer, err) writes them. Tested (example_test.go):
refusal := &auth.Error{Kind: auth.ErrForbidden, Reason: "missing role admin"}
status, response := auth.ResponseFor(fmt.Errorf("open report: %w", refusal))The output is 403 forbidden access denied: the Reason is gone. Your own code returns the same type with its own Code, e.g. &auth.Error{Kind: auth.ErrForbidden, Code: "plan_limit", Reason: "..."}: Code replaces the default code, Reason and the wrapped cause never reach the client.
Stable codes for frontends; the kind decides the status.
| Code | Kind | Raised by |
|---|---|---|
invalid_credentials |
unauthenticated | password login, TOTP enable and disable |
no_pending_login, invalid_totp_code
|
unauthenticated | password TOTP verify (invalid_totp_code also on enable) |
totp_setup_missing |
invalid request | password TOTP enable |
totp_already_enabled, totp_not_enabled
|
conflict | password TOTP setup, enable, disable |
user_exists |
conflict | password register |
weak_password, invalid_username, invalid_email
|
invalid request | password register |
login_expired, state_mismatch
|
invalid request | OIDC and GitHub callback |
login_denied, login_failed
|
unauthenticated | OIDC and GitHub callback |
invalid_api_key |
unauthenticated | apikey |
invalid_token |
unauthenticated |
jwt verifier |
already_impersonating, not_impersonating
|
conflict |
session.Impersonator start, stop |
unknown_impersonation_target, self_impersonation
|
invalid request |
session.Impersonator start |
not_found, unavailable
|
(404, 503) | forward-auth binary only: unknown route, readiness while shutting down or Redis down |
With ErrorURL set, OIDC and GitHub callbacks send the same codes as ?code=<code>, internal included.
type Observer func(ctx context.Context, outcome auth.Outcome), an option on every provider, session.Guard, session.Impersonator, apikey.Authenticator and jwt.Verifier. Called synchronously, at most once per action per request; it must not block.
Outcome{Provider, Action, Result, Err, Identity}:
-
Provider: the provider's name (password, an OIDCName,github,apikey,jwt, orsessionwhen the Guard found no identity). -
Action:login,callback,logout,frontchannel_logout,register,totp_*,refresh_teams,authenticate(per request: Guard,apikey,jwt),refresh(Guard calling a Refresher),impersonation_start,impersonation_stop(session.Impersonator). -
Result:success,challenge(TOTP pending),denied(an*auth.Errorwas answered),error(500). -
ErrwithResultsuccess: a degraded success (e.g. GitHub token revocation failed, a refresh failed but the identity was kept).
Ready-made observers: ginadapter.Observe (fields on the request's wide event, Gin) and prometheusadapter.OutcomeCounter.Observe (<namespace>_auth_outcomes_total{provider,action,result}, Gin). Combine several in one closure.
Tokens, secrets, session IDs, passwords, TOTP codes, emails and names never appear in an error message, an Outcome, a wide-event field or a metric label; IDs (Subject, UserID) do. Errors of outbound calls name method, origin and a classified cause, never path, query or response bytes. The exception is your own wiring: the query parameters you put into the wide-event middleware's QueryAllowlist (see Gin) and the text of your apikey.Resolver and hook errors.
Source: errors.go, response.go, outcome.go, design section 5.
Mirror of docs/wiki at v1.4.0 (5734859). Edit the repository, not this wiki.