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

@javagt/express-easy-auth

v4.1.1

Published

Easy authentication library for Express.js supporting TOTP 2FA, passkeys, and user managed API keys

Readme

express-easy-auth

Full-stack authentication for Express — passwords, TOTP, passkeys, API keys, and session management, wired up in one line.

npm install @javagt/express-easy-auth

Requires Node.js ≥ 22.10.0


Quick Start

import express from 'express';
import { EasyAuth } from '@javagt/express-easy-auth';

const app = express();
app.use(express.json());

const { auth, authManager } = await EasyAuth.create(app, {
    session: { secret: process.env.SESSION_SECRET }
});

app.get('/profile', auth.requireAuth, (req, res) => res.json(req.user));

app.listen(3000);

That's it. Auth routes are live at /auth, sessions are persisted in SQLite at ./data/auth.db, and auth is ready to protect your routes.


What You Get Out of the Box

| Feature | Endpoint(s) | |---|---| | Password login + registration | POST /auth/login, POST /auth/register | | TOTP two-factor auth | POST /auth/totp/* | | Passkeys (WebAuthn) | POST /auth/passkeys/* | | User-managed API keys | GET/POST/PATCH/DELETE /auth/keys | | Session management | GET/DELETE /auth/sessions | | Password reset flow | POST /auth/password-reset/* | | Email verification | POST /auth/verify-email | | SQLite session store | ./data/auth.db (auto-created) | | Rate limiting on sensitive routes | 20 req / 15 min per IP | | Browser client | GET /auth/client.js | | OpenAPI spec | GET /auth/openapi.json |


Configuration

All options are optional. Sane defaults apply everywhere.

const { auth, authManager } = await EasyAuth.create(app, {
    // --- Database ---
    databasePath: './data/auth.db',   // default
    mkdirp:       true,               // auto-create parent dirs (default)

    // --- Session ---
    session: {
        secret:  process.env.SESSION_SECRET,
        cookie:  { maxAge: 7 * 24 * 60 * 60 * 1000 },
    },

    // --- Routing ---
    basePath:      '/auth',   // default
    exposeOpenApi: true,      // serve /auth/openapi.json (default)

    // --- Rate limiting ---
    rateLimit: { windowMs: 15 * 60 * 1000, max: 20 },  // default, or false to disable

    // --- Auth features ---
    requireEmailVerification: false,
    identifierTypes: ['email', 'phone', 'username'],

    // --- Scopes ---
    serverScopes:  ['users.read', 'users.write'],   // app-defined admin capabilities
    projectScopes: ['docs:read', 'docs:write'],     // app-defined project permissions

    // --- WebAuthn ---
    webAuthn: { rpName: 'My App' },

    // --- TOTP ---
    totp: { issuer: 'My App' },
});

Using a pre-built AuthManager

Use the lower-level API when you need the authManager before wiring up the Express app, or for testing:

import { AuthManager, EasyAuth } from '@javagt/express-easy-auth';

const authManager = new AuthManager({ databasePath: './data/auth.db' });
await authManager.init();

const auth = EasyAuth.attach(app, authManager, {
    session: { secret: process.env.SESSION_SECRET }
});

Protecting Routes

The auth object is an AuthMiddleware instance. All methods are pre-bound and can be passed directly to Express.

Require a session

app.get('/me', auth.requireAuth, (req, res) => {
    // req.user    = { id, email, display_name }
    // req.authType = 'session'
    res.json(req.user);
});

Require an API key

app.get('/data', auth.requireApiKey, (req, res) => {
    // req.user   = { id, email, display_name }
    // req.apiKey = { id, name, prefix, grants }
    res.json({ key: req.apiKey.name });
});

Accept either (session or API key)

app.get('/feed', auth.requireAuthOrApiKey, handler);

Require a fresh session

Rejects requests where the user authenticated more than 5 minutes ago. Also rejects API key callers — fresh auth is interactive-only.

app.delete('/account', auth.requireFreshAuth, handler);

Fresh session with a personal scope check

app.post('/me/totp', auth.requireFreshAuth(['personal:auth.write']), handler);

Server scopes (admin capabilities)

Defined by your app in the serverScopes config option. Assigned to users with authManager.grantServerScope().

app.get('/admin/users',
    auth.requireAuth,
    auth.requireServerScope('users.read'),
    handler
);

Personal scopes (account-level access)

Session users implicitly hold all personal scopes. API keys must declare the scope at creation.

app.get('/keys',
    auth.requireAuthOrApiKey,
    auth.requirePersonalScope('personal:apikeys.read'),
    handler
);

Project scopes

Your app resolves the user's permissions for a project and sets req.projectPermissions. The library enforces the ceiling declared on the API key (if any).

async function loadProjectPermissions(req, res, next) {
    const membership = await db.getMembership(req.params.projectId, req.user.id);
    req.projectPermissions = membership?.permissions ?? [];
    next();
}

app.get('/projects/:projectId/docs',
    auth.requireAuthOrApiKey,
    loadProjectPermissions,
    auth.requireProjectAccess('docs:read'),
    handler
);

For API key callers, the effective scope is declared ∩ app-provided permissions. For session callers, it is the app-provided permissions directly.

Project ownership

app.delete('/projects/:projectId',
    auth.requireFreshAuth,
    auth.requireProjectOwner,
    handler
);

Rate limiting

Each rateLimit() call returns an independent limiter with its own bucket.

const tight = auth.rateLimit({ windowMs: 60_000, max: 5 });
app.post('/submit', tight, handler);

req Properties

| Property | Set by | Value | |---|---|---| | req.user | Any auth middleware | { id, email, display_name } | | req.authType | Any auth middleware | 'session' or 'api_key' | | req.lastAuthenticatedAt | Session middleware | Date.now() of last login (ms) | | req.apiKey | API key middleware | { id, name, prefix, grants } | | req.serverScopes | requireServerScope() | Effective server scopes array | | req.effectiveProjectScopes | requireProjectAccess() | Effective project scopes array |


AuthManager API

Use authManager in your own routes, admin panels, background jobs, or tests.

Users

await authManager.registerUser(email, password, displayName?)
await authManager.getUserById(userId)
await authManager.listUsers()
await authManager.deleteUser(userId)
// Returns { deleted: true, warnings: [...] } for orphaned projects

Server Scopes

await authManager.grantServerScope(userId, 'users.read', grantorId?)
await authManager.revokeServerScope(userId, 'users.read')
await authManager.getUserServerScopes(userId)

grantorId must already hold the scope. Pass null for server-side bootstrap.

API Keys

// Create — raw key returned once, never stored
const { key, id, prefix, grants } = await authManager.createApiKey(userId, {
    name:      'CI pipeline',
    expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
    grants: {
        server:   ['users.read'],
        personal: ['personal:profile.read'],
        projects: [{ projectId: 'proj_abc', scopes: ['docs:read'] }],
    },
});

await authManager.listApiKeys(userId)
await authManager.revokeApiKey(userId, keyId)
await authManager.revokeApiKeyAsAdmin(keyId)
await authManager.updateApiKey(userId, keyId, { name?, expiresAt?, clearExpiry? })

Keys sent as Authorization: Bearer sk_... or X-API-Key: sk_....

Projects

// Call when your app creates or deletes a project
await authManager.registerProject(projectId, ownerId)   // idempotent
await authManager.unregisterProject(projectId)

await authManager.isProjectOwner(projectId, userId)
await authManager.getOwnedProjects(userId)
await authManager.transferProjectOwnership(projectId, newOwnerId)

TOTP

const { secret, qrCode } = await authManager.generateTotpSetup(userId)
await authManager.verifyAndEnableTotp(userId, code, secret)
await authManager.disableTotp(userId)
await authManager.getTotpStatus(userId)

Passkeys

const options = await authManager.generateRegistrationOptions(user, cfg)
await authManager.verifyRegistration(user, response, challenge, name, cfg)

const options = await authManager.generateAuthenticationOptions(cfg)
const { user } = await authManager.verifyAuthentication(response, challenge, cfg)

await authManager.getPasskeys(userId)
await authManager.updatePasskeyName(userId, credentialId, name)
await authManager.deletePasskey(userId, credentialId)

Password reset

await authManager.requestPasswordReset(identifier)  // silently no-ops for unknown identifiers
await authManager.resetPassword(token, newPassword)
await authManager.changePassword(userId, newPassword)

Scope taxonomy

// Returns { server: [...], personal: [...], project: [...] }
authManager.getScopeTaxonomy()

Scope System

Three levels

| Level | Who holds it | Stored where | Checked by | |---|---|---|---| | Server | Admin users | DB (user_server_scopes) | requireServerScope() | | Personal | All authenticated users | Implicit (session) / declared (API key) | requirePersonalScope() | | Project | App-managed | App sets req.projectPermissions | requireProjectAccess() |

API key scope ceiling

A key can only be granted scopes the creating user already holds. If a user loses a scope after creation, all their keys immediately lose it too — scope checks are live, not snapshot.

Personal scopes

| Scope | Meaning | API key? | |---|---|---| | personal:profile.read | Read own profile | ✅ | | personal:profile.write | Update display name, email | ✅ | | personal:auth.read | View 2FA / passkey config | ✅ | | personal:auth.write | Change password, configure 2FA | ✅ | | personal:apikeys.read | List own API keys | ✅ | | personal:apikeys.write | Create or revoke keys | ❌ Session only |

personal:apikeys.write is session-only unconditionally. A key can never create other keys.


Custom Adaptors

Database adaptor

Swap out SQLite for any database by extending DatabaseAdaptor:

import { DatabaseAdaptor, EasyAuth } from '@javagt/express-easy-auth';

class PostgresAdaptor extends DatabaseAdaptor {
    async createUser(email, passwordHash, displayName) { /* ... */ }
    // implement remaining abstract methods ...
}

const { auth } = await EasyAuth.create(app, {
    databaseAdapter: new PostgresAdaptor(pool),
    session: { secret: process.env.SESSION_SECRET },
});

Contact adaptor

Deliver verification codes and password-reset links via your own transport:

import { ContactAdaptor, EasyAuth } from '@javagt/express-easy-auth';

class SendgridAdaptor extends ContactAdaptor {
    async sendUserSignupCode(user, code)   { /* send verification email */ }
    async sendUserLoginCode(user, code)    { /* send login code */ }
    async sendUserRecoveryCode(user, code) { /* send password reset link */ }
}

const { auth } = await EasyAuth.create(app, {
    contactAdaptors: [new SendgridAdaptor()],
    session: { secret: process.env.SESSION_SECRET },
});

The default ConsoleContactAdaptor logs codes to the console and redacts them in production.

Challenge store

For multi-instance deployments, replace the default in-memory WebAuthn challenge store:

import { ChallengeStore, EasyAuth } from '@javagt/express-easy-auth';

class RedisStore extends ChallengeStore {
    async set(key, value, ttlMs) { await redis.set(key, JSON.stringify(value), 'PX', ttlMs); }
    async get(key)               { const v = await redis.get(key); return v ? JSON.parse(v) : null; }
    async delete(key)            { await redis.del(key); }
}

const { auth } = await EasyAuth.create(app, {
    challengeStore: new RedisStore(redisClient),
    session: { secret: process.env.SESSION_SECRET },
});

Browser Client

The library ships a ready-to-use browser client. Load it from the auth route:

<script type="module">
import { EasyAuthClient } from '/auth/client.js';

const auth = new EasyAuthClient();
await auth.login('[email protected]', 'password');
console.log(auth.user);  // { id, email, display_name }
</script>

Or install from npm for bundled apps:

import { EasyAuthClient } from '@javagt/express-easy-auth/client';

Key methods: register, login, logout, me, setupTotp, verifyTotp, registerPasskey, loginWithPasskey, createApiKey, listApiKeys, revokeApiKey, changePassword, requestPasswordReset.


All Auth Routes

| Method | Path | Requires | Description | |---|---|---|---| | POST | /auth/register | — | Create account | | POST | /auth/login | — | Password login (+ TOTP if enabled) | | POST | /auth/logout | Session | Destroy current session | | GET | /auth/me | Session or API key | Current identity and auth type | | DELETE | /auth/account | Fresh session | Delete own account | | POST | /auth/verify-email | — | Consume email verification token | | GET | /auth/totp/status | Session | Is TOTP enabled? | | POST | /auth/totp/setup | Fresh session | Begin TOTP setup, returns QR code | | POST | /auth/totp/verify | Fresh session | Confirm code, enables 2FA | | POST | /auth/totp/disable | Fresh session | Disable TOTP | | POST | /auth/passkeys/register/options | Session | Start passkey registration | | POST | /auth/passkeys/register/verify | Session | Complete passkey registration | | POST | /auth/passkeys/login/options | — | Start passkey login | | POST | /auth/passkeys/login/verify | — | Complete passkey login | | POST | /auth/passkeys/verify/options | Session | Start step-up passkey verification | | POST | /auth/passkeys/verify/verify | Session | Complete step-up verification | | GET | /auth/passkeys | Session | List registered passkeys | | PATCH | /auth/passkeys/:id/name | Session | Rename a passkey | | DELETE | /auth/passkeys/:id | Session | Remove a passkey | | GET | /auth/keys | Session or API key | List API keys (metadata only) | | POST | /auth/keys | Session | Create API key | | PATCH | /auth/keys/:id | Session | Update name or expiry | | DELETE | /auth/keys/:id | Session | Revoke API key | | GET | /auth/scopes | — | Available scope taxonomy | | POST | /auth/password-reset/request | — | Send password reset code | | POST | /auth/password-reset/reset | — | Apply reset token, invalidates all sessions | | POST | /auth/password/change | Fresh session | Change password | | GET | /auth/identifiers | Session | List login identifiers | | POST | /auth/identifiers | Session | Add identifier (email, phone, username) | | DELETE | /auth/identifiers/:type/:value | Fresh session | Remove identifier | | GET | /auth/sessions | Session | List active sessions | | DELETE | /auth/sessions/:id | Session | Revoke a specific session | | GET | /auth/client.js | — | Browser client script | | GET | /auth/openapi.json | — | OpenAPI spec |


Running the Demos

npm run start        # demo1 — basic auth + API keys
npm run dev:chat     # demo2 — chat app with scoped identities
npm run dev:docuflow # demo3 — document workflow with project scopes

Design

See DESIGN.md for the full architectural specification: scope system, data model, threat model, and migration guide from v3.