@xenterprises/fastify-xauth-local
v1.2.1
Published
Fastify plugin for JWT authentication with role-based access control - compatible with Express JWT patterns
Readme
@xenterprises/fastify-xauth-local
Fastify 5 plugin for JWT authentication with role-based access control, supporting multiple auth configurations per route prefix. Compatible with express-jwt request.auth patterns.
Install
npm install @xenterprises/fastify-xauth-localQuick 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 decoded JWT payload
fastify.get("/api/profile", async (request) => ({
id: request.auth.id,
email: request.auth.email,
}));
await fastify.listen({ port: 3000 });Plugin Options
| Option | 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:
| Option | 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 — see below |
| getToken | Function | — | No | Custom (request) => token extraction function |
| local | Object | — | No | Local auth routes config — see below |
* One of secret or publicKey/privateKey is required per config.
Local Route Options
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| enabled | boolean | false | No | Enable built-in auth routes |
| loginPath | string | {prefix}/local | No | Login route prefix |
| mePath | string | {loginPath}/me | No | Me route path |
| skipUserLookup | boolean | false | No | If true, /me returns token data without 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 needed.
Local Routes Registered
When local.enabled is true:
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | {loginPath} | No | Login with email/password, returns JWT |
| POST | {loginPath}/register | No | Register new user, returns JWT |
| GET | {loginPath}/me | Yes | Get current user info |
| POST | {loginPath}/password-reset | No | Request password reset |
| PUT | {loginPath}/password-reset | No | Complete reset with token + new password |
Decorated Properties
After registration, fastify.xauthlocal provides:
| Property | Type | Description |
|----------|------|-------------|
| configs | Object | All auth instances keyed by name |
| get(name) | Function | Get a specific config by name |
| password.hash(password, rounds?) | Function | Hash a password with bcrypt |
| password.compare(password, hash) | Function | Compare password against hash |
| config.configCount | number | Number of registered configs |
| config.configNames | string[] | Names of all configs |
Per-Config Instance Properties
Each config (fastify.xauthlocal.get('api')) exposes:
| Property | Type | Description |
|----------|------|-------------|
| name | string | Config name |
| prefix | string | Route prefix |
| jwt.sign(payload, opts?) | Function | Sign a JWT token |
| jwt.verify(token, opts?) | Function | Verify and decode a JWT |
| jwt.decode(token) | Function | Decode without verification |
| jwt.algorithm | string | Algorithm in use (HS256/RS256) |
| requireRole(roles, opts?) | Function | Create role-checking preHandler |
| createMiddleware(opts?) | Function | Create custom auth middleware |
| isExcluded(url, method) | Function | Check if a URL/method is excluded from auth |
| requestProperty | string | Property name on request |
| credentialsRequired | boolean | Whether token is required |
| hasLocalRoutes | boolean | Whether local routes are enabled |
| localPrefix | string|null | Local routes prefix |
| mePath | string|null | Me route path |
Route Exclusions
Supports express-jwt-compatible .unless() style patterns:
excludedPaths: [
"/api/public", // String prefix match
/^\/api\/v\d+\/public/, // Regex match
{ url: "/api/webhook", methods: ["POST"] }, // URL + method filter
{ url: /^\/api\/callback/, methods: ["GET"] }, // Regex URL + method
{ url: "/api/status" }, // URL match, all methods
]Role-Based Access Control
const apiConfig = fastify.xauthlocal.get("api");
// Single role
fastify.get("/api/admin", {
preHandler: [apiConfig.requireRole("admin")],
}, handler);
// Multiple roles (any match)
fastify.get("/api/manage", {
preHandler: [apiConfig.requireRole(["admin", "manager"])],
}, handler);
// Custom role property (default reads from `scope` claim)
fastify.get("/api/special", {
preHandler: [apiConfig.requireRole("editor", { roleProperty: "roles" })],
}, handler);Roles are read from the JWT scope claim (string or array).
Multiple Configs
Each config protects a different route prefix with its own secret/keys and local routes:
await fastify.register(xAuthLocal, {
configs: [
{
name: "api",
prefix: "/api",
secret: process.env.API_SECRET,
local: { enabled: true, userLookup: apiUserLookup, skipUserLookup: true },
},
{
name: "admin",
prefix: "/admin",
secret: process.env.ADMIN_SECRET,
local: { enabled: true, userLookup: adminUserLookup },
},
],
});
// API token won't work for /admin routes and vice versaRSA Keys
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pemconfigs: [{
name: "api",
prefix: "/api",
publicKey: "./keys/public.pem", // File path or key content
privateKey: "./keys/private.pem",
}]Optional Authentication
Set credentialsRequired: false to allow unauthenticated access while still populating request.auth when a token is present:
configs: [{
name: "api",
prefix: "/api",
secret: process.env.JWT_SECRET,
credentialsRequired: false,
}]Environment Variables
The plugin reads no environment variables directly. Pass secrets via config options:
| Variable | Required | Description |
|----------|----------|-------------|
| JWT_SECRET | Yes* | Symmetric secret for HS256 |
| JWT_PUBLIC_KEY | Yes* | Public key for RS256 |
| JWT_PRIVATE_KEY | Yes* | Private key for RS256 |
* Pass one of secret or key pair through your config.
Error Reference
| Status | Error | Context | Message | |--------|-------|---------|---------| | 401 | Unauthorized | Missing token | No authorization token was found | | 401 | Unauthorized | Invalid/expired token | jwt expired / invalid signature / etc. | | 401 | Unauthorized | Login failed | Invalid email or password | | 401 | Unauthorized | Unauthenticated | Authentication required | | 403 | Forbidden | Insufficient role | Access denied. Required role(s): admin | | 409 | Conflict | Registration | An account with this email already exists | | 400 | Bad Request | Password reset | Invalid or expired reset token | | 500 | Internal Server Error | Server error | An error occurred during login/registration |
Named Exports
Available for standalone use:
import {
createJwtService,
createAuthMiddleware,
createRoleMiddleware,
isExcludedRoute,
hashPassword,
comparePassword,
} from "@xenterprises/fastify-xauth-local";How It Works
The plugin registers an onRequest hook per config that checks if the incoming URL starts with that config's prefix. If it does, the hook extracts the JWT from the Authorization: Bearer <token> header (or via getToken), verifies it, and attaches the decoded payload to request[requestProperty]. Routes matching excludedPaths skip verification entirely.
When local.enabled is true, the plugin registers a sub-plugin at loginPath with POST / (login), GET /me, POST /register, POST /password-reset, and PUT /password-reset. These local routes are automatically added to excludedPaths so they don't require a pre-existing token.
Each config maintains its own JWT service, so tokens signed by one config cannot be verified by another — even within the same Fastify instance. The requireRole helper creates preHandler hooks that check the decoded token's scope (or custom roleProperty) against allowed roles.
License
UNLICENSED
