Provides AuthMiddleware (calls httpauth.SetTokenData, accepts Verifier or Signer), IssueTokenPair (access + refresh tokens as jwt.MapClaims, custom claims at top level for ClaimsPermissionProvider compatibility), RefreshTokenPair (blacklist check + rotation + re-issue), and Signer/Verifier implementations for HMAC-SHA256 and RSA-SHA256 including PEM loaders and a public-key-only Verifier for read-only microservices.
4.3 KiB
httpauth-jwt
Self-issued JWT authentication middleware and token management for Go HTTP services.
Part of the code.nochebuena.dev/go ecosystem. Integrates with
httpauth for identity enrichment and
RBAC authorization.
Installation
go get code.nochebuena.dev/go/httpauth-jwt@v1.0.0
go get code.nochebuena.dev/go/httpauth@v0.1.0
Usage
Issue a token pair on login
signer := jwtauth.NewHMACSigner([]byte(os.Getenv("JWT_SECRET")))
cfg := jwtauth.TokenConfig{
AccessTTL: 15 * time.Minute,
RefreshTTL: 7 * 24 * time.Hour,
Issuer: "my-service",
}
// Embed per-resource permission masks — readable by ClaimsPermissionProvider
// without a database call on every request.
customClaims := map[string]any{
"permisos": map[string]any{
"usuarios": int64(515),
"roles": int64(30),
},
}
pair, err := jwtauth.IssueTokenPair(signer, uid, customClaims, cfg)
// pair.AccessToken, pair.RefreshToken, pair.ExpiresIn
Protect routes
import (
jwtauth "code.nochebuena.dev/go/httpauth-jwt"
httpauthmw "code.nochebuena.dev/go/httpauth"
)
r.Use(jwtauth.AuthMiddleware(signer, []string{"/health", "/login", "/refresh"}))
r.Use(httpauthmw.EnrichmentMiddleware(myEnricher))
// Claims-based authz (no DB call — reads from JWT):
claimsProvider := httpauthmw.NewClaimsPermissionProvider("permisos")
r.With(httpauthmw.AuthzMiddleware(claimsProvider, "usuarios", rbac.Permission(1))).
Get("/usuarios", handler)
Rotate tokens on refresh
// blacklist is your Blacklist implementation (e.g. backed by Valkey)
freshClaims := fetchFreshPermissions(ctx, uid)
newPair, err := jwtauth.RefreshTokenPair(ctx, signer, refreshToken, blacklist, cfg, freshClaims)
if errors.Is(err, jwtauth.ErrTokenRevoked) {
// Token was already used — possible replay attack, force re-login
}
RSA — split signing and verification
Services that issue tokens hold the private key; services that only verify hold the public key.
// Issuer service
signer, err := jwtauth.NewRSASignerFromPEM([]byte(os.Getenv("RSA_PRIVATE_KEY_PEM")))
// Verifier-only microservice
verifier, err := jwtauth.NewRSAPublicKeyVerifierFromPEM([]byte(os.Getenv("RSA_PUBLIC_KEY_PEM")))
r.Use(jwtauth.AuthMiddleware(verifier, publicPaths))
Interfaces
Signer
type Signer interface {
Verifier
Sign(claims jwt.Claims) (string, error)
}
Implementations: NewHMACSigner, NewRSASigner, NewRSASignerFromPEM.
Verifier
type Verifier interface {
Verify(tokenString string) (*jwt.Token, error)
}
Every Signer also satisfies Verifier. Standalone: NewRSAPublicKeyVerifier,
NewRSAPublicKeyVerifierFromPEM.
Blacklist
type Blacklist interface {
IsRevoked(ctx context.Context, jti string) (bool, error)
Revoke(ctx context.Context, jti string, ttl time.Duration) error
}
Implement this against Valkey, Redis, or any key-value store that supports TTL.
The TTL on Revoke should match the token's remaining lifetime — entries expire
naturally and the blacklist stays small.
Token structure
Access token claims (standard + custom):
| Claim | Type | Description |
|---|---|---|
sub |
string | User ID |
iss |
string | TokenConfig.Issuer |
iat |
NumericDate | Issued at |
exp |
NumericDate | Expires at |
jti |
string | Unique token ID (UUID v4) |
| (custom) | any | Merged from customClaims at top level |
Refresh token claims:
| Claim | Description |
|---|---|
sub, iss, iat, exp, jti |
Same as access token |
fam |
Token family — UUID v4 for rotation lineage tracking |
Custom claims are intentionally absent from the refresh token. Re-fetch fresh
permission data when issuing the new pair via RefreshTokenPair.
HTTP status codes from AuthMiddleware
| Condition | Status |
|---|---|
Missing or malformed Authorization header |
401 |
| Invalid or expired token | 401 |
Missing sub claim |
401 |
| Public path match | Pass-through (no check) |
Dependencies
| Module | Role |
|---|---|
code.nochebuena.dev/go/httpauth |
SetTokenData — integration contract with EnrichmentMiddleware |
github.com/golang-jwt/jwt/v5 |
JWT signing and parsing |
code.nochebuena.dev/go/rbac |
Transitive via httpauth |