# Authentication

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

`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](https://zenta-dev.github.io/zever/guides/security/password-hashing/) and [Authorization](https://zenta-dev.github.io/zever/guides/security/authz-permissions/).

## Quick start

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

```sh
AUTH_ADAPTER=jwt
AUTH_JWT_SECRET=change-me-to-at-least-32-bytes
```
```sh
AUTH_ADAPTER=jwt
AUTH_JWT_SECRET=prod-secret-at-least-32-bytes-long
```

```go title="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](https://zenta-dev.github.io/zever/reference/config/#auth).

## Key rotation

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.

```sh
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 and call with a bearer token

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

```sh
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 `jti`s 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.

## Production checklist

- CSPRNG secret (32+ bytes) in env or a mounted file, never source. See [Secrets](https://zenta-dev.github.io/zever/guides/security/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](https://zenta-dev.github.io/zever/guides/security/ratelimit/).
- Display config only via `cfg.RedactedServices()` or `config.Redact`.

## Troubleshooting

<details>
<summary>`Open` rejects the JWT secret</summary>

Shorter than 32 bytes (`MinSecretLen`). Set a longer `AUTH_JWT_SECRET`.
</details>

<details>
<summary>All logins return 401 after rotation</summary>

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.
</details>

<details>
<summary>The session adapter fails at startup</summary>

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

<details>
<summary>OIDC login loops</summary>

The provider redirect URL and the callback disagree. Align the app registration with the deployed callback URL.
</details>

## Related

[Authorization](https://zenta-dev.github.io/zever/guides/security/authz-permissions/) · [Password hashing](https://zenta-dev.github.io/zever/guides/security/password-hashing/) · [Crypto and secrets](https://zenta-dev.github.io/zever/guides/security/crypto-secrets/)
