go-auth

Error Handling

Every error the HTTP API returns, grouped by area, with its code, HTTP status, and message.

Error Handling

Most endpoints return errors as JSON with the same shape:

{
  "error": "invalid_credentials",
  "message": "Invalid email or password"
}

error is a stable machine-readable code — match on this, not on message, which is meant for humans and can change wording without notice. The HTTP status carries the same information a REST client expects (404 not found, 409 conflict, 429 rate limited, and so on).

This page covers runtime errors returned by the mounted HTTP handlers. Configuration-time errors — the ones NewConfig returns before your server ever starts — are documented on the Configuration page instead.

Not every layer uses this shape

Two places in the library don't go through the JSON envelope above:

  • CSRF origin/token middleware (middleware.OriginCheck, middleware.CSRFToken) responds with a plain-text body via http.Error — e.g. Forbidden - CSRF headers missing (403) — not JSON. It runs ahead of your handlers and has no domain error to format.
  • Organization membership middleware (middleware.RequireOrgMember, RequireOrgRole) writes JSON, but a narrower one — only {"error": "..."}, no message key.
  • OAuth callback errors are never returned as a JSON body at all — the browser is mid-redirect at that point. They come back as a redirect to {BaseURL}/auth/callback?error={code}&provider={provider}, and your frontend reads error from the query string.

Everything else below — every service-layer and handler-layer error — uses the {"error", "message"} envelope.

General

CodeStatusMessage
internal_error500Internal server error
invalid_json400Invalid request body — malformed JSON on any endpoint that decodes one
invalid_input400Generic input validation failure (exact message varies, e.g. session_ids must not be empty)
forbidden403You do not have permission / Insufficient permissions
rate_limit_exceeded429Too many requests, please try again later
method_disabled405This registration method is not available

internal_error always means: something failed that the caller can't fix by changing their request (a database error, a failed hash, etc.). It's logged server-side with detail; the client only ever sees the generic message.

CodeStatusMessageHint
email_already_exists / account_already_exists409An account with this email already existsBoth codes mean the same thing; register uses the first.
invalid_credentials401Invalid email or passwordDeliberately the same message whether the email doesn't exist or the password is wrong — no account enumeration.
user_banned403This account has been bannedReturned on login and on every authenticated request once a session resolves to a banned user.
email_not_verified403Please verify your email firstOnly when RequireEmailVerification is on.
weak_password400Password must be at least 8 characters (or the specific policy rule that failed)From your configured PasswordPolicy.
invalid_email400Invalid email format
name_required400Name is required
unauthorized401Invalid session / User not foundFrom the auth middleware, not a specific service call.
session_expired401Missing session cookie / Session has expiredAlso from the auth middleware — see Sessions below for the token-level version.

Sessions

CodeStatusMessageHint
session_not_found404Session not foundReturned by revoke-by-ID when the session doesn't exist or belongs to someone else — the two cases are indistinguishable on purpose, so one user can't probe another's session IDs.
session_expired401Session has expired
session_revoked401Session has been revoked
invalid_refresh_token401Refresh token is invalid
refresh_expired401Refresh token has expired
token_already_rotated409This refresh token has already been rotated — use the new oneExpected under concurrent refresh requests; the grace window (SessionConfig.GraceWindow) exists to absorb the common case before this fires.
max_lifetime_exceeded401Session lifetime exceeded, please re-authenticateOnly reachable when SessionConfig.MaxLifetime is set.
invalid_refresh401No refresh token provided
missing_token400Token is required
invalid_input400session_ids must not be empty / cannot revoke more than 100 sessions at onceFrom bulk session revoke.

Password

CodeStatusMessageHint
reset_token_invalid400Invalid password reset token
reset_token_expired410Password reset token has expired
reset_token_already_used410Password reset token has already been used
wrong_password400Password is incorrect / Current password is incorrectUsed both by delete-account and change-password.
no_password400No password set. Use set-password instead.For OAuth-only accounts calling change-password instead of set-password.
already_set400User already has a passwordThe reverse case — calling set-password when one already exists.
invalid_code400Invalid set password code
code_used400Set password code has already been used
email_not_configured500Email sender is not configuredOnly reachable on the confirm-delete-account flow, which requires a mailer explicitly.
email_failed500Failed to send reset email / Failed to send emailThe token was created but delivery failed at the transport level (SMTP error, provider API error).

Account (name, deletion, email verification)

CodeStatusMessageHint
password_required400Password is required to delete account
password_account400Use DELETE /auth/account with password to delete your accountAn OAuth-only account tried the code-confirmation delete flow instead.
delete_code_invalid400Invalid deletion code
delete_code_expired410Deletion code has expired
delete_code_already_used410Deletion code has already been used
validation_error400Name cannot be empty
code_invalid400Invalid verification code
code_already_used410This code has already been used
code_expired410Verification code has expired
already_verified400Email is already verified
email_not_found200If an account exists, a verification email has been sentYes, HTTP 200 — resend-verification never reveals whether the email exists, so a non-existent address gets the same success response as a real one.

Invites (self-service, invite-only signup)

CodeStatusMessage
invite_not_found404Invite not found
invite_expired410This invite has expired
invite_already_used410This invite has already been used
invite_revoked403This invite has been revoked
password_mismatch400Passwords do not match
name_required400Name is required

OAuth

CodeStatusMessageHint
provider_not_found404Unrecognized providerThe {provider} path segment doesn't match any registered WithProvider.
provider_email_unverified403Provider email is not verifiedThe provider returned an email the library doesn't trust as verified.
provider_already_linked / already_linked409This provider is already linked to another accountTwo codes for the same condition at different call sites — treat them the same.
provider_not_linked404This provider is not linked to your account
cannot_unlink_last_provider400Cannot unlink last login method — set a password firstPrevents locking yourself out of an OAuth-only account.
invalid_state400Invalid or expired OAuth stateThe anti-CSRF state token on the callback didn't match.
state_used400OAuth state token already usedState tokens are single-use.
state_expired400OAuth state token has expired
provider_error502Failed to authenticate with providerThe provider's own API rejected the exchange — not something the caller can fix.
unauthorized401Authentication requiredLink/unlink require an existing session.

Callback errors don't arrive as JSON — see Not every layer uses this shape above.

Organizations

CodeStatusMessageHint
org_not_found404Organization not found
org_slug_exists409Organization slug already in use
org_slug_reserved400Organization slug is reserved
org_member_not_found404User is not a member of this organization
org_member_exists409User is already a member of this organization
cannot_remove_last_owner400Cannot remove or demote the last owner of an organization
org_limit_reached400Maximum organization limit reached for userOrganizationConfig.MaxOrgsPerUser.
org_member_limit_reached400Organization member limit reached
org_forbidden403Insufficient organization permissionsReturned by role-gated actions — e.g. a member trying to change another member's role.
org_invite_expired400Organization invite link has expired
org_invite_email_mismatch400Authenticated email does not match invite recipientThe invite was sent to a specific address; whoever accepts it must be logged in as that address.
org_metadata_too_large400Organization metadata exceeds 16KB limit
invalid_slug400Slug must be 255 characters or less
invalid_name400Organization name is required
invalid_role400Invalid organization role
invite_not_found404Invite not foundSame code as the self-service invite system above — org invites reuse it.

The org-membership middleware (RequireOrgMember, RequireOrgRole) returns its own narrower JSON — see Not every layer uses this shape.

Admin

CodeStatusMessageHint
user_not_found404User not foundNo user with that {id} — returned by detail, ban/unban, role change, delete, and the per-user session and audit-log routes.
already_banned400User is already banned
not_banned400User is not banned
last_admin400Cannot ban / demote / delete the last adminThe library refuses to leave zero admins standing.
invalid_role400Role must be 'user' or 'admin'
name_required400Name is required
session_not_found404Session not foundAdmin revoking a specific user session that doesn't exist.
forbidden403Insufficient permissionsFrom RequireRole(domain.RoleAdmin) — a non-admin hit an admin-only route.

CSRF and rate limiting

These respond outside the JSON envelope or with a narrower one — see Not every layer uses this shape for the CSRF middleware specifics.

WhereStatusBody
Missing Origin and Referer, AllowMissingCSRFHeaders: false403Forbidden - CSRF headers missing (plain text)
Origin/Referer present but not allowed403Forbidden (plain text)
Double-submit cookie missing, invalid, or mismatched403Forbidden - CSRF token missing / invalid / mismatch (plain text)
Rate limit exceeded429{"error": "rate_limit_exceeded", "message": "Too many requests, please try again later"} (JSON, uses the normal envelope)

On this page