jwt

jwt issues and verifies HMAC-signed JWTs for HTTP login flows, restricted to HS256/384/512 on the standard library so whole attack classes are unreachable.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/jwt"

Login, token issue, token verification and route authorisation, against net/http.

MethodPurpose
LoginHandlervalidates credentials, issues a token
RenewHandlerrenews a valid token near expiry
IsAuthorizedvalidates a bearer token for a protected handler
Middlewarewraps a handler, injects verified claims into the context
Authenticatevalidates a bearer token, returns claims, for custom middleware
IssueTokenmints a token outside the HTTP login flow
VerifyTokenvalidates a raw token from any transport

IssueToken and VerifyToken are the ones for non-HTTP paths: a WebSocket message, a queue payload, gRPC metadata.

HMAC only, and what that removes

Tokens are RFC 7515 compact JWS with RFC 7519 claims, signed with HMAC-SHA2 (RFC 7518 §3.2): HS256, HS384 or HS512. Nothing else. The implementation is self-contained on the Go standard library.

Narrowing the surface to one algorithm family removes the JWT vulnerabilities that come from algorithm agility rather than from cryptography.

alg=none. The specification permits an unsigned token type. Libraries that honour the header’s alg field have repeatedly accepted a token with alg set to none and no signature at all. There is no code path here that accepts an unsigned token.

RSA-to-HMAC confusion. The classic attack: a server configured for RS256 holds a public key; an attacker submits a token with alg changed to HS256, signed using that public key as the HMAC secret. A library that selects the algorithm from the header verifies it and accepts. Supporting only HMAC means there is no asymmetric key to substitute.

Unverified claim reading. Decoding the payload before checking the signature is trivially forgeable. The API does not expose claims except through a verifying call.

The cost of this narrowing is real: HMAC uses a shared secret, so every service that verifies a token can also mint one. For a fleet of mutually trusting services that is fine. For third-party verification, asymmetric signing is what you need, and this package does not do it.

Credential verification is yours

VerifyCredentialsFn is supplied by the caller, so the package has no opinion on where users live or how passwords are stored. Pair it with passwordhash for Argon2id storage.

Note the cost interaction: Argon2id verification is about 38 ms, so LoginHandler inherits that latency and the memory footprint that comes with it.

Renewal only near expiry

RenewHandler renews a token only when it is close to expiry.

A renew endpoint that refreshes any valid token turns a short-lived token into an unlimited one, since a stolen token can be renewed indefinitely and the expiry stops bounding the damage. Restricting renewal to a window near expiry keeps the original lifetime meaningful.

Short expiry is the primary control, because there is no revocation. A JWT is valid until it expires; nothing the server does invalidates an issued token short of rotating the signing key, which invalidates every token at once.

Costs

Operationns/opB/opallocs/op
IssueToken3,0172,41025
VerifyToken3,3561,89130

Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/jwt/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.

About 3 microseconds either way, roughly 12,000 times cheaper than the Argon2id verification at login. Verifying a token per request is not a cost worth caching around.

Claims from the context

Middleware puts verified claims in the request context; ClaimsFromContext retrieves them. Handlers therefore see claims that have already been verified, with no way to reach an unverified token by accident.

Dependencies

This package reaches no external module: it uses only the Go standard library.