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.
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 callshttpauth.SetTokenDataIssueTokenPair— signs access + refresh token pairsRefreshTokenPair— validates, revokes old JTI, and re-issuesSigner/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:AuthMiddlewareacceptsVerifier— services that only verify useNewRSAPublicKeyVerifierwithout ever touching the private key.jwt.MapClaimsat 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 withhttpauth.ClaimsPermissionProviderand 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. ErrTokenRevokedis 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
AuthMiddlewarewith aSignerwhen aVerifier(public key only) is sufficient. Pass the narrowest interface. - Do not embed permission data in refresh tokens.
RefreshTokenPairre-embeds fresh claims fromcustomClaims— that is the correct place to update permissions. - Do not implement
Blacklistwithout TTL support. Unbounded blacklists grow indefinitely; TTL = remaining token lifetime keeps the store bounded. - Do not import
httpauth-jwtfrom service or domain layers. It is a transport package. Dependency should flow inward: handler → service, never the reverse. - Do not define a local
VerifierorSignerinterface in application code. Use these interfaces directly fromjwtauth.
Testing Notes
compliance_test.gochecks at compile time thatmockSignersatisfiesSigner,mockVerifiersatisfiesVerifier, andmockBlacklistsatisfiesBlacklist.- RSA tests use a package-level
*rsa.PrivateKeygenerated once viamustGenerateRSAto avoid the cost of key generation on every test. TestAuthMiddleware_ExpiredTokenusesAccessTTL: -time.Minuteto produce an already-expired token without sleeping.TestRefreshTokenPair_OldTokenRevokedcallsRefreshTokenPairtwice with the same token — the second call must returnErrTokenRevoked.