Rene Nochebuena d8773b0f9f feat(httpauth-jwt): initial release — self-issued JWT auth middleware v1.0.0
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.
2026-05-07 22:18:04 -06:00

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
Description
No description provided
Readme MIT 60 KiB
2026-05-07 23:53:18 -06:00
Languages
Go 100%