npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-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 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 versa

RSA Keys

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
configs: [{
  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