Package auth/jwt uses only the standard library. Use it when tokens come from an identity provider or must be verified without a database lookup; for first-party APIs the opaque token guard is simpler.
Signing#
key := jwt.HS256("2026-10", secret) // ≥ 32 bytes; also jwt.RS256(kid, rsaKey), jwt.ES256(kid, p256Key), jwt.EdDSA(kid, ed25519Key)
token, err := jwt.Sign(key, jwt.Claims{
Subject: user.AuthID(),
Issuer: "https://app.example.com",
Audience: []string{"api"},
ExpiresAt: time.Now().Add(15 * time.Minute),
})RSA keys need at least 2048 bits. Claims also has NotBefore, IssuedAt, ID and Extra for custom claims.
Verifying#
verifier := jwt.NewVerifier(jwt.StaticKeys(current, previous), jwt.VerifyOptions{
Issuer: "https://app.example.com",
Audience: "api",
})
guard := jwt.NewGuard[models.User]("api", verifier, users) // an auth.Checker
r.Group("/api", func(g *router.Group) {
g.Use(auth.Required(guard))
g.Get("/me", func(c *web.Ctx) error {
claims, _ := jwt.ClaimsFrom(c, "api")
return c.JSON(http.StatusOK, claims)
})
})- The header
algmust be inVerifyOptions.Algorithms(default: all four) and equal the algorithm of the key chosen bykid, so HMAC-with-a-public-key confusion andnoneare impossible. expis required by default (AllowMissingExpiryopts out);nbfandiatare checked with 30s leeway (Leeway);issandaudmatch exactly.- Duplicate JSON members and tokens over 8 KiB are rejected. Every failure wraps
jwt.ErrInvalid, and rejected tokens emit thejwt.rejectedsecurity event.
Key sets#
| Key set | Use |
|---|---|
jwt.StaticKeys(current, previous...) | your own keys, with rotation by kid |
&jwt.RemoteJWKS{URL: issuer + "/.well-known/jwks.json"} | an identity provider's keys: cached, refetched on an unknown kid at most once a minute, refreshed hourly. Needs VerifyOptions.Audience and Issuer (the provider signs tokens for every client with the same keys); without them jwt.NewGuard panics at boot, verifier.Err() says why and every token is refused. Custom key sets wrapping it implement jwt.SharedKeySet (embedding *jwt.RemoteJWKS does). InsecureSkipAudience / InsecureSkipIssuer opt out, dangerously |
jwt.PublicJWKS(keys...) | builds your own JWKS document to publish (HMAC keys are never included) |
Refresh tokens#
refresh := jwt.NewRefreshTokens(db) // migration: jwt.RefreshTokensMigration("<timestamp>_create_refresh_tokens_table")
plain, err := refresh.Issue(ctx, user.AuthID()) // starts a token family
userID, next, err := refresh.Rotate(ctx, plain) // consumes it, returns the user id and a new token
if errors.Is(err, jwt.ErrRefreshReused) { /* the whole family was revoked */ }
err = refresh.Revoke(ctx, plain) // logout
err = refresh.RevokeUser(ctx, userID) // password changeRefresh tokens are opaque; only their SHA-256 is stored. Rotation is atomic, so concurrent rotations have exactly one winner. Presenting a consumed token returns jwt.ErrRefreshReused and revokes the whole family.