Skip to content
Zever
Home

Authentication

Issue and verify caller identity with JWT, session, or OIDC behind one interface.

2 min readEdit on GitHub

auth answers who is calling, never what they may do. It covers jwt (auth/jwt), session (auth/session), and oidc (auth/oidc); see Password hashing and Authorization.

Set a 32+ byte JWT secret, then resolve the service; Open refuses shorter secrets and config.Default() ships none.

Terminal window
AUTH_ADAPTER=jwt
AUTH_JWT_SECRET=change-me-to-at-least-32-bytes
main.go
c := container.New(cfg)
a, err := c.Auth()
if err != nil {
return err // secret too short, or adapter unavailable
}

AUTH_JWT_SECRET maps to options.jwt.secret (env splits at the first underscore). Keys: jwt.issuer, jwt.audience, jwt.max_ttl, jwt.leeway, jwt.keys, jwt.active_kid, oidc.issuer, oidc.client_id, oidc.timeout, oidc.allow_insecure. See the auth config reference.

List every accepted key with a kid and point active_kid at the signer. New tokens carry the kid header; verification selects the key by kid and still accepts kid-less tokens signed with the legacy secret, so old clients keep working through the overlap. Unknown kids fail closed. Keep a retired key listed until every token signed with it has expired, then remove it. HS256 only; asymmetric algorithms (EdDSA/RS256) are not supported — use a custom auth adapter if you need them. alg: none tokens are rejected.

Terminal window
AUTH_ADAPTER=jwt
AUTH_JWT_SECRET=previous-secret-at-least-32-bytes-long
AUTH_JWT_KEYS_- KID=2026-09
AUTH_JWT_KEYS_SECRET=previous-secret-at-least-32-bytes-long
AUTH_JWT_ACTIVE_KID=2026-10
AUTH_JWT_LEEWAY=30s

Tokens carrying a future nbf (not-before) claim are rejected; exp is always required. Both honor leeway, which defaults to 0 (strict).

Log in, then send the token on every other route:

Terminal window
curl -s -X POST localhost:8080/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"correct horse battery staple"}'
curl -s localhost:8080/api/spaces -H "Authorization: Bearer $TOKEN"

Login returns 200 with a signed JWT in token; register 201; missing or invalid tokens 401. Issue mints, Verify checks per request, Revoke invalidates a token (jwt keeps revoked jtis in a per-process store; session uses its store). Verify returns distinct ErrInvalidToken and ErrTokenExpired errors; map both to one generic 401 so responses leak nothing. Log details server-side, never token values.

  • CSPRNG secret (32+ bytes) in env or a mounted file, never source. See Secrets.
  • Rotate with overlap: add the new key to jwt.keys, flip jwt.active_kid, and only remove the old key after its tokens have expired.
  • Prefer session for immediate revocation across instances; JWT revocation is per-process by default (the memory store dies with the process and is invisible to other instances), so multi-instance deployments must inject the shared revocation/redis store via auth.JWTOptions.RevocationStore — otherwise revoked tokens stay valid on sibling instances until TTL.
  • Rate-limit login; hashing cost is not an attempt limit. See Rate limiting.
  • Display config only via cfg.RedactedServices() or config.Redact.
Open rejects the JWT secret

Shorter than 32 bytes (MinSecretLen). Set a longer AUTH_JWT_SECRET.

All logins return 401 after rotation

Old-secret tokens no longer verify: either the retired kid was removed from jwt.keys before its tokens expired, or active_kid points at a key ID with no matching entry. Restore the old key (or fix active_kid), or log in again for a fresh token.

The session adapter fails at startup

It needs an injected session.Store (Go-only, not config). Wire a shared store for multiple instances.

OIDC login loops

The provider redirect URL and the callback disagree. Align the app registration with the deployed callback URL.

Authorization · Password hashing · Crypto and secrets