Files
httpauth-jwt/CLAUDE.md
Rene Nochebuena b9a5cc2f92 fix(httpauth-jwt)!: rename package httpauthjwt, bump httpauth and rbac to v1.0.0
Rename package from jwtauth to httpauthjwt to follow ecosystem convention
(repo name = package name, hyphens removed). Bump httpauth dependency from
v0.1.0 to v1.0.0 and rbac indirect dependency from v0.9.0 to v1.0.0.

BREAKING CHANGE: import path unchanged (code.nochebuena.dev/go/httpauth-jwt)
but package identifier changes from jwtauth to httpauthjwt — update all usages
accordingly.
2026-05-07 23:51:16 -06:00

4.3 KiB

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

  • VerifierSigner: 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):

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):

verifier, err := jwtauth.NewRSAPublicKeyVerifierFromPEM([]byte(os.Getenv("RSA_PUBLIC_KEY_PEM")))
r.Use(jwtauth.AuthMiddleware(verifier, publicPaths))

Full middleware stack:

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:

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.