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

monolite-auth

v0.9.1

Published

Optional, pluggable authentication: login, JWT tokens, password hashing and route guards.

Readme

monolite-auth

Authentication you can plug your own everything into.

The package brings the parts that are identical in every application — the login flow, a token BLL, a password hasher, the middleware that guards a route — and deliberately refuses to know the parts that are not. Where your users live is IUserProvider, which you implement. How passwords are hashed is IPasswordHasher, with a dependency-free implementation included. What a token is made of is ITokenBLL, with JWT as the default.

Nothing here is mandatory: no other package in the toolkit depends on this one, and an application that already authenticates somewhere else can take requireAuth alone.

Install

npm install monolite-auth

It expects express@^5 in the host application, and builds on monolite-core and monolite-http.

Usage

import {
  AuthController,
  AuthBLL,
  JwtTokenBLL,
  ScryptPasswordHasher,
  requireAuth,
  requireRoles,
} from "monolite-auth";

const tokens = new JwtTokenBLL({ secret: process.env.JWT_SECRET!, expiresIn: "1h" });
const hasher = new ScryptPasswordHasher();
const auth = new AuthBLL(new SqlUserProvider(repository), hasher, tokens);

// The login route. `guards` is optional; a rate limiter belongs there.
const controller = new AuthController(auth, [loginRateLimiter]);

// Guarding routes.
app.use("/api", requireAuth(tokens, { context }));
app.use("/api/admin", requireRoles("admin"));

POST /auth/login answers with the token, its lifetime in seconds and the identity behind it:

{
  "token": "eyJhbGciOi...",
  "expiresIn": 3600,
  "user": { "id": "42", "name": "Ana", "email": "[email protected]", "roles": ["admin"] }
}

Plugging in your own user provider

One method. Return null when there is no such user — never throw for "not found", because telling a missing user apart from a broken lookup is exactly what the login flow refuses to leak.

import type { AuthUserWithSecret, IUserProvider } from "monolite-auth";

export class SqlUserProvider implements IUserProvider {
  constructor(private readonly users: IGenericRepository<IUser>) {}

  async findByEmail(email: string): Promise<AuthUserWithSecret | null> {
    const user = await this.users.firstOrDefault({ where: { email } });
    if (!user) return null;

    return {
      id: String(user.pkUser),
      name: user.name,
      email: user.email,
      roles: user.roles?.split(",") ?? [],
      passwordHash: user.passwordHash,
    };
  }
}

AuthBLL lower-cases and trims the address before handing it over, so store and query your emails lower-cased.

Plugging in your own hasher

ScryptPasswordHasher is the default because crypto.scrypt ships with Node: the package hashes passwords without dragging in a native module that needs a toolchain, breaks on Node upgrades and has to be rebuilt per platform. scrypt is memory-hard (RFC 7914) — the property that matters against GPU cracking — and OWASP accepts it for password storage. It is a defensible default, not a claim that it is the best available.

Its cost parameters live inside each stored hash (scrypt$N$r$p$salt$hash), so raising them later does not invalidate the passwords already stored:

// OWASP's current guidance, for a process with the memory headroom for it.
new ScryptPasswordHasher({ cost: 2 ** 17 });

To use argon2id or bcrypt instead, implement the same two methods:

import argon2 from "argon2";
import type { IPasswordHasher } from "monolite-auth";

export class Argon2PasswordHasher implements IPasswordHasher {
  hash(plain: string): Promise<string> {
    return argon2.hash(plain, { type: argon2.argon2id });
  }

  async verify(plain: string, hash: string): Promise<boolean> {
    // The contract is `false`, never a throw, for an unreadable hash.
    try {
      return await argon2.verify(hash, plain);
    } catch {
      return false;
    }
  }
}

Pass it to AuthBLL and nothing else changes. Because every hash carries its scheme at the front, old and new can coexist while stored passwords are migrated on next login.

What login does not tell you

A wrong password and an unknown email produce the same status, the same code and the same message. Telling them apart would turn the login form into a lookup service — submit a list of addresses with junk passwords, learn which ones are registered — and knowing that somebody has an account is often the sensitive part on its own.

The message is only half of it. The password check runs even when there is no such user, against a throwaway hash built by your own hasher, so that the two answers take the same time to arrive. Skipping it would make "unknown email" come back in a millisecond and "wrong password" in the hundred that hashing deliberately costs, and that difference is measurable across a network.

Guarding routes

requireAuth(tokenBLL, options?) reads Authorization: Bearer, verifies the token and publishes the identity — on the request, and into the request context when you pass one, which is what lets a BLL three layers down know who is asking without threading the user through every signature.

requireAuth(tokens, {
  context,
  // For an issuer whose claims the toolkit does not recognise. The default is
  // `toCurrentUser` from monolite-http, which already knows what OIDC and
  // Azure AD emit — and is the same function the request context is built on.
  toCurrentUser: (claims) => ({ id: claims.uid as string, name: "...", email: null, roles: [] }),
});

requireRoles(...roles) grants access when the user holds any of the listed roles — the reading of [Authorize(Roles = ...)] in ASP.NET, and the way the rule is usually said out loud. To demand several at once, chain the middleware:

app.use("/api/invoices", requireRoles("admin"), requireRoles("billing"));

Both answer through AppError, so a 401 or a 403 from here carries the same shape — code, request id — as every other error in the API.

No refresh endpoint

There is no POST /auth/refresh, on purpose. Refreshing honestly means a second, longer-lived credential that is stored, rotated on every use and revocable; without that store there is nothing to revoke, and an endpoint that merely re-signs a still-valid access token does not extend a session so much as delete the expiry that was the point of having one. The store is a decision about your persistence, which is precisely what this package refuses to know.

An application that wants refresh tokens has everything it needs: ITokenBLL signs and verifies whatever payload you give it, including a long-lived one whose id you keep in your own table.

API

| Export | What it is | | --- | --- | | AuthUser, AuthUserWithSecret, Credentials, AuthResult, TokenClaims, SignedToken | The vocabulary | | IUserProvider, IPasswordHasher, ITokenBLL, IAuthBLL | The four seams | | AuthBLL, AuthBLLOptions | The login flow | | JwtTokenBLL, JwtTokenBLLOptions | Signing and verifying JWTs | | ScryptPasswordHasher, ScryptPasswordHasherOptions | The dependency-free hasher | | requireAuth, requireRoles, authenticatedUser, RequireAuthOptions | Route guards | | AuthController, loginSchema, authResultSchema | POST /auth/login | | AUTH_TOKENS, AuthToken | The DI identifiers, which is what you register your own IUserProvider under |

License

MIT