Skip to content

auth-jwt capability

auth-jwt adds JWT authentication middleware to your service, validating tokens on protected routes and making claims available to handlers downstream.

Server-side sessions require shared state — every instance of your service needs to reach the same session store. That’s a coupling point that complicates horizontal scaling and increases operational overhead. JWTs move that state into the token itself: the token is signed, self-contained, and verifiable by any instance that has the signing key.

This works well for microservices where multiple services need to verify identity independently. Each service validates the token signature locally, extracts the claims it needs (user ID, roles, tenant ID), and proceeds without a network hop to a central session store.

The tradeoff is that JWTs can’t be individually revoked without additional infrastructure (a token denylist, usually backed by Redis). For most APIs this is acceptable — short token lifetimes combined with refresh token rotation cover the majority of revocation needs. If you need per-token revocation with no latency, you want sessions, not JWTs.

  • Middleware that validates JWT tokens on protected routes
  • Claims extraction into request context (Go) or request object (TypeScript)
  • signToken and verifyToken utility functions
  • Bearer token extraction from the Authorization header
  • Route-level opt-in: unprotected routes are unaffected

The Go capability uses golang-jwt/jwt — the maintained fork of the original dgrijalva/jwt-go package. The scaffold provides an AuthMiddleware function that validates the token signature, checks expiry, and stores claims in the request context.

// Middleware wraps protected route groups.
protected := router.Group("/api", middleware.Auth(cfg.JWT.Secret))
// Handlers retrieve claims from context — no need to re-parse the token.
claims, ok := middleware.ClaimsFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
userID := claims.Subject

Token signing is handled by a SignToken function that takes a Claims struct and returns the signed string. The signing algorithm defaults to HS256 — adequate for most services. If you need asymmetric signing (so multiple services can verify without sharing a secret), switch to RS256 by passing an RSA private key.

The middleware distinguishes between an invalid signature (returns 401) and an expired token (returns 401 with a specific error code that clients can use to trigger a refresh).

The TypeScript capability uses jose — a comprehensive JWT library that implements the JOSE standards (JWS, JWE, JWK, JWT) with full TypeScript types and support for both symmetric and asymmetric algorithms.

The scaffold exports three functions: signToken, verifyToken, and extractBearerToken. These compose into the auth middleware.

import { signToken, verifyToken, extractBearerToken } from './auth/jwt';
// Sign a token at login time.
const token = await signToken(
{ sub: user.id, role: user.role },
{ expiresIn: '1h' }
);
// Middleware extracts and verifies on every protected request.
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
const raw = extractBearerToken(req);
if (!raw) return res.status(401).json({ error: 'missing token' });
const claims = await verifyToken(raw);
if (!claims) return res.status(401).json({ error: 'invalid token' });
req.user = claims;
next();
}

jose uses the Web Crypto API under the hood, which is available natively in Node.js 18+ and all modern runtimes. No native bindings, no build-time issues.

If your API serves browser clients, JWT auth and CORS interact at preflight. The browser sends an OPTIONS request before the actual request — that preflight must succeed without an auth check, or the browser will never send the credentialed request. The scaffold configures CORS to exclude OPTIONS from middleware processing.

For multi-tenant services, extract the tenant ID from JWT claims rather than from request headers. A tenant ID in a header is trivially forged; a tenant ID inside a signed token is not. See multi-tenancy for how this pattern is enforced.

Requires: http-api

Suggests: cors, rate-limiting

Terminal window
verikt new my-service --cap http-api,auth-jwt
# or add to an existing service:
verikt add auth-jwt

cors

CORS headers for browser clients authenticating with JWT.

rate-limiting

Rate limiting to protect authentication endpoints.

multi-tenancy

Tenant isolation using authenticated identity from JWT claims.

oauth2

OAuth2 flows that issue JWT tokens.