fastify-xauth-local
fastify-xauth-local
JWT authentication with role-based access control (RBAC) for Fastify 5. Supports multiple independent auth configurations per route prefix, HS256 symmetric secrets and RS256 key pairs, optional built-in login/register/password-reset routes, and express-jwt-compatible request.auth patterns.
Installation
npm install @xenterprises/fastify-xauth-local
Quick Start
import Fastify from "fastify";
import xAuthLocal from "@xenterprises/fastify-xauth-local";
const fastify = Fastify();
await fastify.register(xAuthLocal, {
configs: [
{
name: "api",
prefix: "/api",
secret: process.env.JWT_SECRET,
excludedPaths: ["/api/public", "/api/health"],
local: {
enabled: true,
userLookup: async (email) => db.users.findByEmail(email),
createUser: async (userData) => db.users.create(userData),
},
},
],
});
// Protected route — request.auth contains the decoded JWT payload
fastify.get("/api/profile", async (request) => ({
id: request.auth.id,
email: request.auth.email,
}));
await fastify.listen({ port: 3000 });
Options
Plugin Options
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
active | boolean | true | No | Enable/disable the plugin entirely |
configs | Array | — | Yes | Array of auth configuration objects |
basePath | string | process.cwd() | No | Base path for resolving relative key file paths |
Config Options
Each entry in the configs array:
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | Yes | Unique identifier for this config |
prefix | string | — | Yes | Route prefix to protect (e.g. /api) |
secret | string | — | Yes* | Symmetric secret for HS256 |
publicKey | string | — | Yes* | Public key content or file path for RS256 |
privateKey | string | — | Yes* | Private key content or file path for RS256 |
algorithm | string | auto | No | RS256 when keys provided, HS256 for secret |
expiresIn | string | '4d' | No | Default token expiration (e.g. '1h', '7d') |
audience | string | — | No | JWT audience claim for sign/verify |
issuer | string | — | No | JWT issuer claim for sign/verify |
requestProperty | string | 'auth' | No | Property name on request for decoded token |
credentialsRequired | boolean | true | No | Whether a token is required (false = optional auth) |
excludedPaths | Array | [] | No | Paths/patterns to skip auth |
getToken | Function | — | No | Custom (request) => token extraction function |
local | Object | — | No | Built-in auth routes — see Local Route Options |
* One of secret or publicKey/privateKey is required per config.
Local Route Options
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
enabled | boolean | false | No | Enable built-in login/register/reset routes |
loginPath | string | {prefix}/local | No | Route prefix for local auth |
mePath | string | {loginPath}/me | No | Path for the /me endpoint |
skipUserLookup | boolean | false | No | /me returns token data without a DB call |
userLookup | Function | — | Yes** | async (email) => user | null |
createUser | Function | — | No | async (userData) => newUser |
passwordReset | Function | — | No | async (email, token?, hashedPassword?) => void |
saltRounds | number | 10 | No | bcrypt salt rounds for password hashing |
** Required when local.enabled is true and login is used.
Methods
JWT Service (per config instance)
Access via fastify.xauthlocal.get('name').jwt:
- jwt.sign — Sign a payload and return a JWT string.
- jwt.verify — Verify a JWT and return the decoded payload.
- jwt.decode — Decode a JWT without signature verification.
RBAC & Middleware (per config instance)
Access via fastify.xauthlocal.get('name'):
- requireRole — Create a
preHandlerthat enforces role membership. - createMiddleware — Create a custom auth middleware with override options.
- isExcluded — Check whether a URL/method is excluded from auth for this config.
Password Utilities
Access via fastify.xauthlocal.password:
- password.hash — Hash a plaintext password with bcrypt.
- password.compare — Compare a plaintext password against a bcrypt hash.
Built-In Local Routes
Registered at {loginPath} when local.enabled is true:
- local.login —
POST {loginPath}— Authenticate with email + password. - local.register —
POST {loginPath}/register— Create a new user account. - local.me —
GET {loginPath}/me— Return the current authenticated user. - local.password-reset —
POST/PUT {loginPath}/password-reset— Request or complete a password reset.
Error Reference
All authentication errors use standard HTTP status codes. The error prefix in logs is xAuthLocal.
| Status | Message | Context |
|---|---|---|
| 401 | No authorization token was found | Missing JWT in request |
| 401 | jwt expired / invalid signature | Invalid or expired token |
| 401 | Invalid email or password | Login attempt failed |
| 401 | Authentication required | Unauthenticated request on protected route |
| 403 | Access denied. Required role(s): <roles> | Insufficient role via requireRole |
| 409 | An account with this email already exists | Registration conflict |
| 400 | Invalid or expired reset token | Password reset with bad token |
| 500 | An error occurred during login/registration | Unexpected server error |
Environment Variables
The plugin reads no environment variables directly. Pass secrets through config options:
| Variable | Required | Description |
|---|---|---|
JWT_SECRET | Yes* | Symmetric secret for HS256 — pass as configs[].secret |
JWT_PUBLIC_KEY | Yes* | Public key for RS256 — pass as configs[].publicKey |
JWT_PRIVATE_KEY | Yes* | Private key for RS256 — pass as configs[].privateKey |
* Provide either JWT_SECRET or the RSA key pair per config.
How It Works
On registration the plugin iterates the configs array and creates an independent jsonwebtoken-backed JWT service and auth middleware for each entry. A global onRequest hook checks whether the incoming URL starts with each config's prefix; if so, it extracts the bearer token (or calls getToken), verifies it against that config's secret or public key, and attaches the decoded payload to request[requestProperty]. Routes matching excludedPaths skip verification. When local.enabled is true, a sub-plugin is registered at loginPath providing login, register, and password-reset routes — these paths are automatically added to excludedPaths. Each config is stored on fastify.xauthlocal by name, so a token signed by one config cannot be verified by another.
AI Context
package: "@xenterprises/fastify-xauth-local"
type: fastify-plugin
use-when: JWT auth with RBAC for Fastify — HS256/RS256, multiple route prefixes, optional built-in login/register/password-reset routes
decorator: fastify.xauthlocal
request-decorator: request.auth (decoded JWT payload, property name configurable)
env: JWT_SECRET (HS256), JWT_PUBLIC_KEY / JWT_PRIVATE_KEY (RS256) — pass via config options, not read directly
built-in-routes: POST {loginPath}, POST {loginPath}/register, GET {loginPath}/me, POST {loginPath}/password-reset (when local.enabled: true)
