Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/jwt"
Package jwt provides an HTTP-oriented JWT authentication helper for username/password login flows: validate user credentials, issue short-lived signed JWTs, authorize protected endpoints from an Authorization header, and optionally renew tokens near expiration.
The API is compatible with net/http:
- [JWT.LoginHandler]: validates credentials and issues signed JWTs.
- [JWT.RenewHandler]: renews a valid token only when it is close to expiry.
- [JWT.IsAuthorized]: validates bearer tokens for protected handlers.
- [JWT.Middleware]: wraps a handler, injecting verified claims into the
request context for retrieval via
ClaimsFromContext. - [JWT.Authenticate]: validates a bearer token and returns its claims, for building custom middleware.
- [JWT.IssueToken]: mints a token outside the HTTP login flow, once the caller has verified the user’s identity by its own means.
- [JWT.VerifyToken]: validates a raw token string that arrived over any transport (WebSocket messages, queue payloads, gRPC metadata).
Credential verification is delegated to a caller-provided
VerifyCredentialsFn, so the package is agnostic to the password-hashing
scheme; use the OWASP-compliant github.com/tecnickcom/nurago/pkg/passwordhash
(Argon2id) for storage.
Implementation
Tokens are RFC 7515 compact JWS with RFC 7519 claims, signed with HMAC-SHA2
(RFC 7518 §3.2: HS256, HS384 or HS512). The implementation is self-contained on
the Go standard library. Restricting the surface to
symmetric HMAC makes the classic JWT attacks (alg=none, asymmetric-to-HMAC
confusion) structurally impossible: the accepted algorithm is pinned and the
signature is verified before the claims payload is ever decoded. A crit header
(RFC 7515 §4.1.11) or a duplicated header parameter is rejected; other unknown
JOSE header parameters (typ, kid, …) are ignored. The exp and nbf time
claims are validated (with optional leeway); iat is decoded but not validated,
as only a key holder could forge it.
Authentication Flow
- The login endpoint decodes JSON credentials (
username,password). - A user-provided
VerifyCredentialsFnchecks them against the user store. - On success, a JWT is signed with configured claims and returned as text.
- Downstream handlers validate
Authorization: Bearer <token>viaJWT.IsAuthorized,JWT.Middleware,JWT.Authenticate, orJWT.RenewHandler.
Claims and Defaults
By default the package issues short-lived HMAC-signed tokens with:
- expiration:
DefaultExpirationTime(5 minutes) - renew window:
DefaultRenewTime(30 seconds before expiry) - header name:
DefaultAuthorizationHeader(Authorization) - request body cap:
DefaultMaxBodyBytes - token size cap:
DefaultMaxTokenBytes
Issued tokens include standard registered claims (exp, iat, nbf, jti,
sub) and an auth_time claim recording the original login. They support
optional iss and aud via options. The sub (Subject) claim is set to the
authenticated username. When iss and/or aud are configured, they are also
enforced during verification: a token missing them, or carrying different
values, is rejected.
Extension Points
Functional options allow custom behavior without replacing core handlers:
- response output customization (
WithSendResponseFn) - token/header settings (
WithExpirationTime,WithRenewTime,WithAuthorizationHeader,WithSigningMethod,WithMaxBodyBytes,WithMaxTokenBytes) - session controls (
WithMaxSessionLifetime,WithClockSkewLeeway) - key rotation (
WithPreviousKeys) - claim metadata (
WithClaimIssuer,WithClaimAudience) - logger customization (
WithLogger)
Security Notes
- Only HMAC signing methods (HS256/HS384/HS512) are supported: the same
symmetric key both signs and verifies. Keep it secret, and at least as long
as the signing method’s hash output (enforced by
New). - To rotate the signing key without invalidating outstanding sessions, deploy
the new key while listing the old one in
WithPreviousKeys, then drop the old key once the rotation window (expiration time plus renew window) has elapsed. - Return uniform error messages for invalid credentials to avoid account
enumeration (the default login path already does this). A
VerifyCredentialsFnMUST also equalize its own timing between known and unknown users so existence does not leak through response latency. - Use HTTPS so bearer tokens are never exposed in transit.
- The handlers do not restrict the HTTP method; the caller is responsible for routing login to POST and protected endpoints appropriately.
- Tokens are stateless: there is no server-side revocation before
exp, and renewing a token does not invalidate the previous one, which stays valid until its own expiration. Configure short expiration windows appropriate for your threat model, and bound how long a session may be kept alive by renewals withWithMaxSessionLifetime. - The package does not rate-limit or lock out repeated failed logins; brute-force protection (rate limiting, lockout, CAPTCHA) must be layered by the caller.
- The default responder logs the full response body, including the issued
token, at debug level. Where debug logs are retained, disable them or pass a
redacting logger via
WithLogger(see github.com/tecnickcom/nurago/pkg/redact, which detects JWT compact serializations).
When To Use
- A service authenticates with username and password and issues short-lived tokens.
- Protected endpoints authorize from the Authorization header.
- Tokens should be renewable near expiry without a fresh login.
Example
ph := passwordhash.New()
// Pre-computed password hashes, as they would be stored in a user database.
aliceHash, err := ph.PasswordHash("s3cr3t-pw")
if err != nil {
fmt.Println(err)
return
}
users := map[string]string{"alice": aliceHash}
// A decoy hash to verify against for unknown users, so response timing does
// not reveal whether an account exists.
decoyHash, err := ph.PasswordHash("decoy-pw")
if err != nil {
fmt.Println(err)
return
}
verify := func(username, password string) (bool, error) {
hash, ok := users[username]
if !ok {
_, _ = ph.PasswordVerify(password, decoyHash)
return false, nil
}
return ph.PasswordVerify(password, hash)
}
auth, err := jwt.New([]byte("0123456789abcdef0123456789abcdef"), verify)
if err != nil {
fmt.Println(err)
return
}
ctx := context.Background()
// Log in with valid credentials and capture the issued token.
loginRec := httptest.NewRecorder()
loginReq := httptest.NewRequestWithContext(ctx, http.MethodPost, "/login", strings.NewReader(`{"username":"alice","password":"s3cr3t-pw"}`))
auth.LoginHandler(loginRec, loginReq)
fmt.Println("login status:", loginRec.Code)
token, _ := io.ReadAll(loginRec.Result().Body)
// Protect an endpoint with the middleware: the verified claims are available
// from the request context, identifying the caller.
protected := auth.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, _ := jwt.ClaimsFromContext(r.Context())
fmt.Fprintln(w, "hello", claims.Subject)
}))
authRec := httptest.NewRecorder()
authReq := httptest.NewRequestWithContext(ctx, http.MethodGet, "/protected", nil)
authReq.Header.Set(httputil.HeaderAuthorization, httputil.HeaderAuthBearer+string(token))
protected.ServeHTTP(authRec, authReq)
fmt.Println("protected status:", authRec.Code)
fmt.Print(authRec.Body.String())
// Output:
// login status: 200
// protected status: 200
// hello alice
Full source is in example_jwt_test.go. More runnable examples are on pkg.go.dev.
Dependencies
Importing this package pulls 1 external module:
github.com/julienschmidt/httprouter