Skip to content

Add JWT Auth

This guide shows the minimum steps to add JWT-based authentication to a Plumego service. You will wire a JWTManager, protect routes with middleware/auth, and issue token pairs from a login handler.

For the boundary rationale behind these packages, see the Security Primer.

  • Constructing a JWTManager with a KV-backed key store
  • Generating an access/refresh token pair
  • Protecting routes with authmw.Authenticate
  • Verifying tokens in a handler when needed

Place the manager in your application’s dependency struct so it can be injected explicitly.

import (
"github.com/spcent/plumego/security/jwt"
kvstore "github.com/spcent/plumego/store/kv"
)
store, err := kvstore.NewKVStore(kvstore.Options{DataDir: "/var/lib/myapp/jwt"})
if err != nil {
return fmt.Errorf("jwt store: %w", err)
}
cfg := jwt.DefaultJWTConfig()
manager, err := jwt.NewJWTManager(store, cfg)
if err != nil {
return fmt.Errorf("jwt manager: %w", err)
}

DefaultJWTConfig sets access tokens to 15 minutes and refresh tokens to 7 days with HMAC-SHA256 signing. Override any field on cfg before calling NewJWTManager.

Step 2 — Protect routes with the auth middleware

Section titled “Step 2 — Protect routes with the auth middleware”

In your RegisterRoutes method, apply authmw.Authenticate to the routes that require a valid token.

import (
authmw "github.com/spcent/plumego/middleware/auth"
"github.com/spcent/plumego/security/jwt"
)
// app.Use applies middleware globally; wrap individual routes for finer control.
authMw, err := authmw.Authenticate(a.Manager.Authenticator(jwt.TokenTypeAccess))
if err != nil {
return err
}
a.Core.Use(authMw)

To protect only a subset of routes, wrap the handler directly instead of calling Use:

protected, err := authmw.Authenticate(a.Manager.Authenticator(jwt.TokenTypeAccess))
if err != nil {
return err
}
a.Core.Get("/api/profile",
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
protected(http.HandlerFunc(profileHandler)).ServeHTTP(w, r)
}),
)

Step 3 — Issue a token pair from a login handler

Section titled “Step 3 — Issue a token pair from a login handler”
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
// Validate credentials — your domain logic here.
// ...
identity := jwt.IdentityClaims{Subject: userID}
authz := jwt.AuthorizationClaims{Roles: []string{"user"}}
pair, err := h.Manager.GenerateTokenPair(r.Context(), identity, authz)
if err != nil {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeInternal).
Message("could not issue token").
Build())
return
}
_ = contract.WriteResponse(w, r, http.StatusOK, pair, nil)
}

Step 4 — Verify a token explicitly when needed

Section titled “Step 4 — Verify a token explicitly when needed”

Most handlers rely on the middleware to reject invalid tokens before they run. When a handler needs to inspect claims directly:

claims, err := h.Manager.VerifyToken(r.Context(), tokenString, jwt.TokenTypeAccess)
if err != nil {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeUnauthorized).
Message("invalid token").
Build())
return
}
// claims.Subject, claims.Roles, etc. are now available.
  • Token signing and key rotation live in security/jwt, not in your handler.
  • The auth middleware is transport-only: it validates and enriches the request context; it does not contain business logic.
  • GenerateTokenPair returns both access and refresh tokens so the client can renew without re-authenticating.
  • Secrets are never logged; verification fails closed on any error.
SymptomCheck first
Protected route still allows anonymous requestsConfirm authmw.Authenticate(...) wraps the route or is registered with Use before Prepare
Valid token is rejectedConfirm the middleware and issuer use the same JWTManager, key store, and jwt.TokenTypeAccess
Handler needs claimsVerify the token with h.Manager.VerifyToken(...) or read the middleware-provided context value exposed by the auth package
Errors leak secret materialNever log tokens, signing keys, or raw authorization headers
Failure behavior is inconsistentTreat every verification, parsing, and key lookup error as unauthorized or forbidden; fail closed

The reference service shows JWT wiring in context:

Run the reference service locally and hit the auth routes to see the complete flow:

Terminal window
git clone https://github.com/spcent/plumego
cd plumego/reference/standard-service
go run .