# 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`.