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.1.2

Published

Fastify plugin for JWT authentication with role-based access control - compatible with Express JWT patterns

Downloads

375

Readme

xAuthLocal

Fastify 5 plugin for JWT authentication with role-based access control, supporting multiple authentication configurations for different route prefixes.

Features

  • Multiple Auth Configs: Separate authentication for different route prefixes (e.g., /api, /admin, /portal)
  • JWT Authentication: RS256 (RSA keys) or HS256 (symmetric secret) support per config
  • Route Exclusions: Express-jwt compatible .unless() style patterns
  • Role-Based Access Control: Support for multiple roles via scope claim
  • Local Auth Routes: Built-in login, register, me, and password-reset endpoints per config
  • Skip User Lookup: Option to use token data only for /me endpoint (no database call)
  • Backwards Compatible: Uses request.auth pattern familiar from express-jwt

Installation

npm install xauthlocal jsonwebtoken bcryptjs

Quick Start

Single Config

import Fastify from "fastify";
import xAuthLocal from "xauthlocal";

const fastify = Fastify();

await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      secret: process.env.JWT_SECRET,
      excludedPaths: ["/api/public", "/api/health"],
    },
  ],
});

// Protected route
fastify.get("/api/users", async (request) => {
  // request.auth contains the decoded JWT payload
  return { userId: request.auth.id };
});

await fastify.listen({ port: 3000 });

Multiple Configs

await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      secret: process.env.API_SECRET,
      local: {
        enabled: true,
        userLookup: async (email) => db.users.findByEmail(email),
        createUser: async (userData) => db.users.create(userData),
        skipUserLookup: true, // /me returns token data, no DB call
      },
    },
    {
      name: "admin",
      prefix: "/admin",
      secret: process.env.ADMIN_SECRET,
      local: {
        enabled: true,
        userLookup: async (email) => db.admins.findByEmail(email),
        skipUserLookup: false, // /me fetches fresh data from DB
      },
    },
    {
      name: "portal",
      prefix: "/portal",
      publicKey: "./keys/portal-public.pem",
      privateKey: "./keys/portal-private.pem",
    },
  ],
});

// Each config protects routes under its prefix
// Tokens are config-specific (API token won't work for admin routes)

Configuration Options

Plugin Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | configs | Array | Yes | - | Array of auth configurations | | basePath | string | No | process.cwd() | Base path for relative key paths | | active | boolean | No | true | Enable/disable the plugin |

Config Options

Each config in the configs array supports:

| Option | Type | Required | Default | 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 path for RS256 | | privateKey | string | Yes* | - | Private key content or path for RS256 | | algorithm | string | No | Auto | 'RS256' for keys, 'HS256' for secret | | expiresIn | string | No | '4d' | Default token expiration | | audience | string | No | - | JWT audience claim | | issuer | string | No | - | JWT issuer claim | | excludedPaths | Array | No | [] | Paths to exclude from auth | | requestProperty | string | No | 'auth' | Property to attach decoded token | | credentialsRequired | boolean | No | true | Whether token is required | | getToken | Function | No | - | Custom token extraction function | | local | Object | No | - | Local routes configuration |

*One of secret or publicKey/privateKey is required per config.

Local Route Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | enabled | boolean | No | false | Enable local auth routes | | loginPath | string | No | {prefix}/local | Login route path | | mePath | string | No | {loginPath}/me | Me route path | | skipUserLookup | boolean | No | false | Use token data only for /me | | userLookup | Function | Yes** | - | Function to lookup user by email | | createUser | Function | No | - | Function to create new user | | passwordReset | Function | No | - | Function to handle password reset | | saltRounds | number | No | 10 | bcrypt salt rounds |

**Required if local routes are enabled.

Route Exclusions

Exclude routes from authentication using patterns compatible with express-jwt:

await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      secret: process.env.JWT_SECRET,
      excludedPaths: [
        // String prefix match
        "/api/public",
        "/api/health",

        // Regex match
        /^\/api\/v\d+\/public/,

        // Object with URL and optional methods
        { url: "/api/webhook", methods: ["POST"] },
        { url: /^\/api\/callback/, methods: ["GET", "POST"] },

        // Object without methods (matches all methods)
        { url: "/api/status" },
      ],
    },
  ],
});

Role-Based Access Control

Use the requireRole helper to protect routes by role:

const apiConfig = fastify.xauthlocal.get("api");

// Single role required
fastify.get(
  "/api/admin/dashboard",
  { preHandler: [apiConfig.requireRole("admin")] },
  async (request) => ({ dashboard: true })
);

// Multiple roles (any match)
fastify.get(
  "/api/manage/users",
  { preHandler: [apiConfig.requireRole(["admin", "manager"])] },
  async (request) => ({ users: [] })
);

Roles are read from the scope claim in the JWT payload:

const apiConfig = fastify.xauthlocal.get("api");
const token = apiConfig.jwt.sign({
  id: 1,
  email: "[email protected]",
  scope: ["admin", "user"], // Array of roles
});

Local Auth Routes

When local.enabled is true, the following endpoints are registered:

| Method | Path | Auth Required | Description | |--------|------|---------------|-------------| | POST | {prefix}/local | No | Login with email/password | | GET | {prefix}/local/me | Yes | Get current user | | POST | {prefix}/local/register | No | Register new user | | POST | {prefix}/local/password-reset | No | Request password reset | | PUT | {prefix}/local/password-reset | No | Complete password reset |

skipUserLookup Option

Control whether /me endpoint makes a database call:

{
  name: "api",
  prefix: "/api",
  secret: process.env.JWT_SECRET,
  local: {
    enabled: true,
    userLookup: async (email) => db.users.findByEmail(email),
    skipUserLookup: true, // Returns token data directly, no DB call
  },
}
  • skipUserLookup: true: Returns user data stored in the JWT token (faster, no DB call)
  • skipUserLookup: false: Fetches fresh user data from database via userLookup (default)

Login Request/Response

// POST /api/local
// Request
{
  "email": "[email protected]",
  "password": "password123"
}

// Response
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe",
    "admin": false,
    "scope": ["user"]
  }
}

API

After registration, access the plugin via fastify.xauthlocal:

// Get all configs
fastify.xauthlocal.configs; // { api: {...}, admin: {...} }

// Get specific config by name
const apiConfig = fastify.xauthlocal.get("api");

// JWT Service (per config)
apiConfig.jwt.sign(payload, options);
apiConfig.jwt.verify(token, options);
apiConfig.jwt.decode(token);

// Role middleware factory (per config)
apiConfig.requireRole("admin");
apiConfig.requireRole(["admin", "manager"]);

// Custom middleware factory (per config)
apiConfig.createMiddleware({
  excludedPaths: ["/special"],
  credentialsRequired: false,
});

// Check if route is excluded (per config)
apiConfig.isExcluded("/api/public/health", "GET"); // true

// Config info (per config)
apiConfig.name; // "api"
apiConfig.prefix; // "/api"
apiConfig.hasLocalRoutes; // true
apiConfig.localPrefix; // "/api/local"
apiConfig.mePath; // "/api/local/me"

// Password utilities (global)
await fastify.xauthlocal.password.hash("password123");
await fastify.xauthlocal.password.compare("password123", hash);

// Summary config (read-only)
fastify.xauthlocal.config;
// {
//   configCount: 2,
//   configNames: ["api", "admin"]
// }

Using RSA Keys

Generate keys:

# Generate private key
openssl genrsa -out private.pem 2048

# Generate public key
openssl rsa -in private.pem -pubout -out public.pem

Configure with key paths or content:

// Using file paths
await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      publicKey: "./keys/public.pem",
      privateKey: "./keys/private.pem",
    },
  ],
});

// Using key content (e.g., from environment)
await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      publicKey: process.env.JWT_PUBLIC_KEY,
      privateKey: process.env.JWT_PRIVATE_KEY,
    },
  ],
});

Optional Authentication

Allow routes to work with or without authentication:

await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      secret: process.env.JWT_SECRET,
      credentialsRequired: false,
    },
  ],
});

fastify.get("/api/posts", async (request) => {
  if (request.auth) {
    // Authenticated user - return personalized content
    return getPersonalizedPosts(request.auth.id);
  }
  // Anonymous user - return public content
  return getPublicPosts();
});

Complete Example

import Fastify from "fastify";
import xAuthLocal from "xauthlocal";

const fastify = Fastify({ logger: true });

// In-memory users for demo
const users = new Map();
const admins = new Map();

await fastify.register(xAuthLocal, {
  configs: [
    {
      name: "api",
      prefix: "/api",
      secret: process.env.API_SECRET || "api-development-secret",
      expiresIn: "7d",
      excludedPaths: ["/api/health", { url: "/api/public", methods: ["GET"] }],
      local: {
        enabled: true,
        userLookup: async (email) => users.get(email),
        createUser: async (userData) => {
          const user = { id: Date.now(), ...userData, scope: ["user"] };
          users.set(userData.email, user);
          return user;
        },
        passwordReset: async (email) => {
          console.log(`Password reset requested for ${email}`);
        },
        skipUserLookup: true, // Fast /me endpoint
      },
    },
    {
      name: "admin",
      prefix: "/admin",
      secret: process.env.ADMIN_SECRET || "admin-development-secret",
      expiresIn: "1d",
      local: {
        enabled: true,
        userLookup: async (email) => admins.get(email),
        skipUserLookup: false, // Always fetch fresh admin data
      },
    },
  ],
});

// Public routes
fastify.get("/api/health", async () => ({ status: "ok" }));
fastify.get("/api/public/info", async () => ({ version: "1.0.0" }));

// Protected API routes
fastify.get("/api/profile", async (request) => ({
  id: request.auth.id,
  email: request.auth.email,
}));

// Admin-only route
const adminConfig = fastify.xauthlocal.get("admin");
fastify.get(
  "/admin/stats",
  { preHandler: [adminConfig.requireRole("admin")] },
  async () => ({ users: users.size, admins: admins.size })
);

await fastify.listen({ port: 3000 });

console.log("Server running on http://localhost:3000");
console.log("API: POST /api/local to login, POST /api/local/register to create account");
console.log("Admin: POST /admin/local to login");

Testing

npm test

License

ISC