@xenterprises/fastify-xauth-jwks
v1.2.1
Published
Fastify plugin for path-based JWT/JWKS validation. Protect multiple paths with independent JWKS providers.
Readme
@xenterprises/fastify-xauth-jwks
Path-based JWT/JWKS validation for Fastify v5. Protect multiple API paths with independent JWKS providers.
Installation
npm install @xenterprises/fastify-xauth-jwksQuick Start
import Fastify from "fastify";
import xAuth from "@xenterprises/fastify-xauth-jwks";
const fastify = Fastify();
await fastify.register(xAuth, {
paths: {
admin: {
pathPattern: "/admin",
jwksUrl: "https://your-auth.com/.well-known/jwks.json",
excludedPaths: ["/health"],
},
portal: {
pathPattern: "/portal",
jwksUrl: "https://your-auth.com/.well-known/jwks.json",
},
},
});
// Protected route - requires valid JWT
fastify.get("/admin/dashboard", (request) => {
return { userId: request.auth.userId, user: request.user };
});
// Excluded route - no auth required
fastify.get("/admin/health", () => ({ status: "ok" }));
// Public route - not under any protected path
fastify.get("/public/info", () => ({ info: "open" }));
await fastify.listen({ port: 3000 });Options
The plugin accepts a single paths object where each key is a path name and each value is a path configuration:
| Option | Type | Default | Required | Description |
|---|---|---|---|---|
| paths | object | — | Yes | Map of path name to path configuration. Must contain at least one entry. |
Path Configuration
| Option | Type | Default | Required | Description |
|---|---|---|---|---|
| pathPattern | string | /<pathName> | No | URL prefix to protect. All routes starting with this prefix require authentication. |
| jwksUrl | string | — | One of jwksUrl / jwksData | Remote JWKS endpoint URL for token verification. |
| jwksData | object | — | One of jwksUrl / jwksData | Local JWKS data ({ keys: [...] }) or a single JWK object for development/testing. |
| active | boolean | true | No | Set to false to skip registration of this path. |
| excludedPaths | string[] | [] | No | Sub-paths that bypass authentication (e.g., ["/health", "/docs"]). |
| jwksCooldownDuration | number | 30000 | No | Minimum milliseconds between JWKS refetches (remote only). |
| jwksCacheMaxAge | number | 1800000 | No | Maximum age in milliseconds for cached JWKS keys (remote only). |
| enablePayloadCache | boolean | true | No | Cache verified JWT payloads in memory to avoid re-verification. |
| payloadCacheTTL | number | 300000 | No | Time-to-live in milliseconds for cached JWT payloads. |
Decorated Properties
After registration, the plugin decorates fastify.xAuth:
| Property | Type | Description |
|---|---|---|
| fastify.xAuth.validators | object | Map of path name to validator object. |
Validator Object
Each validator in fastify.xAuth.validators.<name> exposes:
| Property / Method | Type | Description |
|---|---|---|
| name | string | Path name (e.g., "admin"). |
| pathPattern | string | URL prefix being protected. |
| jwksUrl | string \| undefined | Remote JWKS URL if configured. |
| config | object | Caching configuration values. |
| verifyJWT(token) | async function | Verify a JWT string. Returns the payload object or null. |
| clearPayloadCache() | function | Clear the in-memory JWT payload cache. |
| getPayloadCacheStats() | function | Returns { size, enabled, ttl } for monitoring. |
Request Properties
On authenticated requests, the plugin sets:
| Property | Type | Description |
|---|---|---|
| request.user | object | Full JWT payload (claims). |
| request.auth.path | string | Name of the path that authenticated this request. |
| request.auth.userId | string | The sub claim from the JWT. |
| request.auth.payload | object | Full JWT payload (same as request.user). |
Utility Exports
Import from @xenterprises/fastify-xauth-jwks/utils:
import {
extractToken,
hasRole,
hasPermission,
getUserId,
getAuthEndpoint,
requireRole,
requirePermission,
requireEndpoint,
decodeToken,
decodeHeader,
} from "@xenterprises/fastify-xauth-jwks/utils";| Utility | Signature | Description |
|---|---|---|
| extractToken(request) | (req) => string \| null | Extract Bearer token from Authorization header. |
| hasRole(user, roles) | (user, string \| string[]) => boolean | Check if user has any of the specified roles (reads user.roles). |
| hasPermission(user, perms) | (user, string \| string[]) => boolean | Check if user has any of the specified permissions (reads user.permissions). |
| getUserId(request) | (req) => string \| null | Get user ID from request.auth.userId or request.user.sub. |
| getAuthEndpoint(request) | (req) => string \| null | Get the auth path name from request.auth.path. |
| requireRole(roles) | (string \| string[]) => preHandler | Fastify preHandler that returns 403 if user lacks the role. |
| requirePermission(perms) | (string \| string[]) => preHandler | Fastify preHandler that returns 403 if user lacks the permission. |
| requireEndpoint(name) | (string) => preHandler | Fastify preHandler that returns 403 if request was not authenticated by the named path. |
| decodeToken(token) | (string) => object | Decode JWT payload without verification (re-export of jose.decodeJwt). |
| decodeHeader(token) | (string) => object | Decode JWT header without verification (re-export of jose.decodeProtectedHeader). |
Usage with Route Hooks
import { requireRole, requirePermission } from "@xenterprises/fastify-xauth-jwks/utils";
fastify.get("/admin/settings", {
preHandler: requireRole("admin"),
handler: async (request) => ({ settings: "..." }),
});
fastify.delete("/admin/users/:id", {
preHandler: requirePermission("users:delete"),
handler: async (request) => ({ deleted: true }),
});Environment Variables
The plugin itself does not read environment variables. Your application should pass JWKS URLs from environment variables:
| Variable | Required | Description |
|---|---|---|
| ADMIN_JWKS_URL | Per path | JWKS endpoint for admin path validation. |
| PORTAL_JWKS_URL | Per path | JWKS endpoint for portal path validation. |
Example:
await fastify.register(xAuth, {
paths: {
admin: {
pathPattern: "/admin",
jwksUrl: process.env.ADMIN_JWKS_URL,
},
},
});Error Reference
| HTTP Status | Error | When |
|---|---|---|
| 401 | Access token required | No Authorization: Bearer <token> header present on a protected route. |
| 401 | Invalid token | Token failed JWKS verification, is expired, or missing sub claim. |
| 401 | Authentication failed | Unexpected error during authentication (logged server-side). |
| 403 | Insufficient permissions | requireRole or requirePermission check failed. |
| 403 | Must authenticate via <name> endpoint | requireEndpoint check failed. |
Startup errors (thrown during plugin registration):
| Error Message | Cause |
|---|---|
| xAuth: options object is required | No options passed to the plugin. |
| xAuth: 'paths' option is required and must contain at least one path configuration | Empty or missing paths option. |
| <name>: Either jwksUrl or jwksData is required | Path config missing both JWKS source options. |
| <name>: Cannot specify both jwksUrl and jwksData | Path config has both JWKS source options. |
How It Works
Registration: For each entry in
paths, the plugin creates a path validator. Remote JWKS endpoints are set up viajose.createRemoteJWKSetwith configurable caching; local JWKS data usesjose.createLocalJWKSet.Request Hook: An
onRequesthook checks every incoming request URL against each registeredpathPatternusingString.startsWith(). If the URL matches a protected path and is not inexcludedPaths, the hook extracts the Bearer token and verifies it against the path's JWKS.Caching: Two levels of caching are used. JWKS keys are cached by the
joselibrary with configurable cooldown and max age. JWT payloads can optionally be cached in aMapkeyed by the raw token string, with configurable TTL.Request Decoration: On successful verification,
request.useris set to the full JWT payload andrequest.authis set with the path name, user ID, and payload for downstream route handlers.
Tests
npm test
# 63 tests passingLicense
UNLICENSED
