• v1.0.0 b9a5cc2f92

    Rene Nochebuena released this 2026-05-07 23:53:18 -06:00 | 1 commits to main since this release

    v1.0.0

    code.nochebuena.dev/go/httpauth-jwt

    Overview

    httpauth-jwt provides self-issued JWT authentication for Go HTTP services. It
    covers the full token lifecycle: issuing access + refresh pairs, rotating refresh
    tokens through a Blacklist interface, and verifying Bearer tokens in an
    AuthMiddleware that integrates directly with the httpauth middleware stack.

    This is the JWT counterpart to httpauth-firebase — both call httpauth.SetTokenData
    after token verification, making all downstream middleware (EnrichmentMiddleware,
    AuthzMiddleware, ClaimsPermissionProvider) completely provider-agnostic.

    Released at v1.0.0 because it is designed for immediate production use across
    multiple services.

    What's Included

    Signing and verification

    NewHMACSigner(secret []byte) Signer — HMAC-SHA256. For single-service
    deployments.

    NewRSASigner(privateKey *rsa.PrivateKey) Signer — RSA-SHA256. For deployments
    where the token issuer must be separated from verifiers.

    NewRSASignerFromPEM(pemKey []byte) (Signer, error) — loads PKCS#8 or PKCS#1
    PEM private key from an environment variable or file.

    NewRSAPublicKeyVerifier(publicKey *rsa.PublicKey) Verifier and
    NewRSAPublicKeyVerifierFromPEM(pemKey []byte) (Verifier, error) — for
    microservices that verify tokens but never issue them. AuthMiddleware accepts the
    Verifier interface, so these services need no signing capability.

    Token issuance

    IssueTokenPair(signer, uid, customClaims, cfg) (TokenPair, error)

    customClaims are merged at the top level of the access token. This is the mechanism
    for embedding per-resource permission masks:

    customClaims := map[string]any{
        "permisos": map[string]any{"usuarios": int64(515)},
    }
    

    The httpauth.ClaimsPermissionProvider("permisos") reads these directly — no
    database call on every request. On refresh, callers re-fetch fresh masks and embed
    them in the new token.

    The refresh token carries only identity (sub, iss, iat, exp, jti) and a
    token family (fam). It never carries permission data.

    Refresh rotation

    RefreshTokenPair(ctx, signer, refreshToken, blacklist, cfg, customClaims) (TokenPair, error)

    Full rotation protocol:

    1. Verify the refresh token signature and expiry.
    2. Check the JTI against the Blacklist.
    3. If revoked → return ErrTokenRevoked (possible replay attack).
    4. Revoke the old JTI with the token's remaining TTL.
    5. Issue a new pair with customClaims in the access token.

    ErrTokenRevoked — sentinel error. Callers should respond with 401 and
    prompt re-authentication when they see this.

    Blacklist interfaceIsRevoked and Revoke. Implementations backed by
    Valkey or Redis are the expected use case. TTL on Revoke keeps the blacklist
    bounded — entries naturally expire when the old token would have expired anyway.

    AuthMiddleware

    AuthMiddleware(verifier Verifier, publicPaths []string) func(http.Handler) http.Handler

    Verifies Bearer tokens and calls httpauth.SetTokenData(ctx, uid, claims). Accepts
    Verifier (not Signer) — the middleware only needs verification capability.

    Design Highlights

    VerifierSigner. AuthMiddleware accepts the narrower Verifier interface.
    A service that receives tokens from a central issuer passes a NewRSAPublicKeyVerifier
    — it never touches the private key. A service that also issues tokens passes its
    Signer directly (which embeds Verifier).

    Claims at the top level. Access tokens use jwt.MapClaims with custom claims
    merged at the top level, not nested under a wrapper key. This makes them directly
    compatible with httpauth.ClaimsPermissionProvider and any standard JWT introspection
    tool.

    Refresh tokens carry no permission data. Permissions are re-fetched and
    re-embedded on each rotation. This ensures role changes take effect on the next
    refresh without requiring active token revocation for access tokens.

    Blacklist TTL = remaining token lifetime. Revoking a JTI with the token's
    remaining lifetime means the blacklist entry expires at the exact moment the token
    would have expired naturally. The blacklist stays small and requires no cleanup.

    ErrTokenRevoked is a sentinel. Callers use errors.Is(err, httpauthjwt.ErrTokenRevoked)
    to distinguish a replay attack from a general infrastructure error. Other errors
    indicate connectivity or signing faults and may warrant different handling.

    Installation

    go get code.nochebuena.dev/go/httpauth-jwt@v1.0.0
    go get code.nochebuena.dev/go/httpauth@v1.0.0
    
    Downloads