• v1.0.2 941b8972ed

    Rene Nochebuena released this 2026-05-18 14:39:23 -06:00 | 0 commits to main since this release

    v1.0.2

    code.nochebuena.dev/go/httpauth-jwt

    Overview

    v1.0.2 adds jwt.WithJSONNumber() to all Verify implementations, standardizes
    AuthMiddleware error responses to JSON, and bumps the httpauth dependency to
    v1.0.2 to consume wildcard resource fallback, json.Number precision handling, and
    the shared WriteJSONError helper.

    No breaking changes. All v1.0.x consumers can upgrade with go get.

    What Changed Since v1.0.1

    jwt.WithJSONNumber() in all Verify implementations

    All three Verify implementations now pass jwt.WithJSONNumber() to jwt.Parse:

    • hmacSigner.Verify
    • rsaSigner.Verify
    • rsaPublicVerifier.Verify

    Without this option, the JWT library decodes all numbers as float64. float64
    cannot represent math.MaxInt64 = 9223372036854775807 exactly — it rounds to 2^63 = 9223372036854775808, which overflows back to math.MinInt64 when cast to int64.
    This would corrupt the ADMIN wildcard permission mask stored in JWT claims.

    With jwt.WithJSONNumber(), numbers are preserved as json.Number (a decimal string
    wrapper). json.Number.Int64() parses the exact value — no float64 round-trip, no
    overflow. httpauth.ClaimsPermissionProvider (v1.0.2) handles all three types —
    int64, float64, json.Number — so the chain is fully covered.

    JSON error body in AuthMiddleware

    AuthMiddleware now returns a structured JSON body for all 401 responses by calling
    httpauth.WriteJSONError. Previously it called http.Error which writes text/plain.

    The full middleware stack now produces consistent error responses:

    Middleware Condition Status Code
    AuthMiddleware Missing/invalid/expired token 401 UNAUTHENTICATED
    EnrichmentMiddleware No uid in context 401 UNAUTHENTICATED
    EnrichmentMiddleware Enricher returns error 500 INTERNAL
    AuthzMiddleware No identity in context 401 UNAUTHENTICATED
    AuthzMiddleware Permission denied or provider error 403 PERMISSION_DENIED

    Dependency bump

    code.nochebuena.dev/go/httpauth v1.0.1 → v1.0.2

    Migration

    No breaking changes. Callers that inspect decoded JWT claims should note that numeric
    values now come back as json.Number instead of float64.

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

    Rene Nochebuena released this 2026-05-07 23:53:18 -06:00 | 2 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