# 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`](https://code.nochebuena.dev/go/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 ```go 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 ```go 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 ```go // 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. ```go // 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` ```go type Signer interface { Verifier Sign(claims jwt.Claims) (string, error) } ``` Implementations: `NewHMACSigner`, `NewRSASigner`, `NewRSASignerFromPEM`. ### `Verifier` ```go type Verifier interface { Verify(tokenString string) (*jwt.Token, error) } ``` Every `Signer` also satisfies `Verifier`. Standalone: `NewRSAPublicKeyVerifier`, `NewRSAPublicKeyVerifierFromPEM`. ### `Blacklist` ```go 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` |