Files
httpauth-jwt/CLAUDE.md

104 lines
4.3 KiB
Markdown
Raw Permalink Normal View History

# httpauth-jwt
Self-issued JWT authentication middleware and token management.
## Purpose
`httpauth-jwt` covers the full JWT lifecycle for HTTP services that issue their own
tokens (as opposed to delegating to Firebase or another IdP). It provides:
- `AuthMiddleware` — verifies Bearer JWTs and calls `httpauth.SetTokenData`
- `IssueTokenPair` — signs access + refresh token pairs
- `RefreshTokenPair` — validates, revokes old JTI, and re-issues
- `Signer` / `Verifier` — HMAC-SHA256 and RSA-SHA256 implementations + PEM loaders
The JWT counterpart to `httpauth-firebase`. Both call `httpauth.SetTokenData` after
verification — all downstream middleware in `httpauth` is provider-agnostic.
## Tier & Dependencies
**Tier:** 4 (transport auth layer; depends on Tier 3 `httpauth` and `jwt/v5`)
**Module:** `code.nochebuena.dev/go/httpauth-jwt`
**Package name:** `httpauthjwt`
**Direct imports:** `code.nochebuena.dev/go/httpauth`, `github.com/golang-jwt/jwt/v5`
**Transitive:** `code.nochebuena.dev/go/rbac` (indirect, via `httpauth`)
## Key Design Decisions
- **`Verifier``Signer`**: `AuthMiddleware` accepts `Verifier` — services that
only verify use `NewRSAPublicKeyVerifier` without ever touching the private key.
- **`jwt.MapClaims` at the top level**: Custom claims are merged at the top level of
the access token (not under a wrapper key). This makes them directly compatible
with `httpauth.ClaimsPermissionProvider` and standard JWT tools.
- **Refresh tokens carry no permissions**: Re-fetching on each rotation ensures role
changes take effect without revoking access tokens.
- **Blacklist TTL = remaining token lifetime**: Entries expire naturally; no cleanup
job needed. Implementations backed by Valkey/Redis set the TTL on `Revoke`.
- **`ErrTokenRevoked` is a sentinel**: `errors.Is(err, jwtauth.ErrTokenRevoked)`
distinguishes replay attacks from infrastructure errors.
## Patterns
**Issuer service (HMAC or RSA private key):**
```go
signer := jwtauth.NewHMACSigner([]byte(os.Getenv("JWT_SECRET")))
// or:
signer, err := jwtauth.NewRSASignerFromPEM([]byte(os.Getenv("RSA_PRIVATE_KEY_PEM")))
cfg := jwtauth.TokenConfig{AccessTTL: 15 * time.Minute, RefreshTTL: 7 * 24 * time.Hour, Issuer: "svc"}
pair, err := jwtauth.IssueTokenPair(signer, uid, customClaims, cfg)
```
**Verifier-only microservice (RSA public key):**
```go
verifier, err := jwtauth.NewRSAPublicKeyVerifierFromPEM([]byte(os.Getenv("RSA_PUBLIC_KEY_PEM")))
r.Use(jwtauth.AuthMiddleware(verifier, publicPaths))
```
**Full middleware stack:**
```go
r.Use(jwtauth.AuthMiddleware(signer, publicPaths))
r.Use(httpauthmw.EnrichmentMiddleware(myEnricher))
claimsProvider := httpauthmw.NewClaimsPermissionProvider("permisos")
r.With(httpauthmw.AuthzMiddleware(claimsProvider, "usuarios", rbac.Permission(1))).
Get("/usuarios", handler)
```
**Refresh endpoint:**
```go
newPair, err := jwtauth.RefreshTokenPair(ctx, signer, body.RefreshToken, bl, cfg, freshClaims)
if errors.Is(err, jwtauth.ErrTokenRevoked) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
```
## What to Avoid
- Do not use `AuthMiddleware` with a `Signer` when a `Verifier` (public key only)
is sufficient. Pass the narrowest interface.
- Do not embed permission data in refresh tokens. `RefreshTokenPair` re-embeds fresh
claims from `customClaims` — that is the correct place to update permissions.
- Do not implement `Blacklist` without TTL support. Unbounded blacklists grow
indefinitely; TTL = remaining token lifetime keeps the store bounded.
- Do not import `httpauth-jwt` from service or domain layers. It is a transport
package. Dependency should flow inward: handler → service, never the reverse.
- Do not define a local `Verifier` or `Signer` interface in application code.
Use these interfaces directly from `jwtauth`.
## Testing Notes
- `compliance_test.go` checks at compile time that `mockSigner` satisfies `Signer`,
`mockVerifier` satisfies `Verifier`, and `mockBlacklist` satisfies `Blacklist`.
- RSA tests use a package-level `*rsa.PrivateKey` generated once via `mustGenerateRSA`
to avoid the cost of key generation on every test.
- `TestAuthMiddleware_ExpiredToken` uses `AccessTTL: -time.Minute` to produce an
already-expired token without sleeping.
- `TestRefreshTokenPair_OldTokenRevoked` calls `RefreshTokenPair` twice with the
same token — the second call must return `ErrTokenRevoked`.