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

handshake-auth

v0.2.1

Published

Lightweight, storage-agnostic authentication for Express.js

Readme

Handshake Auth

Lightweight, storage-agnostic authentication for Express.js.

Overview

Handshake Auth is a lightweight authentication library that follows the strategy pattern similar to Passport.js, but without requiring a database connection. You provide callbacks for all storage operations (Inversion of Control), giving you complete control over how accounts are stored and retrieved.

Core Philosophy

"Handshake Auth authenticates requests. It does not log users in."

The library handles the "handshakes" and "proofs" without demanding a seat at your database table. After successful authentication, you receive an Account object and decide what to do with it.

Why Handshake Auth?

vs Passport.js

Passport is the de facto standard but shows its age:

| Passport.js | Handshake Auth | |-------------|----------------| | Middleware hides control flow (magic redirects) | Explicit control flow (you call authenticate(), you handle the result) | | Sessions are the assumed default | Session-agnostic (works with cookie-session, no server-side state) | | serializeUser/deserializeUser are global singletons | Callbacks are per-instance, not global | | Callback-based API (done(err, user, info)) | async/await throughout | | OAuth flows are opaque | OAuth phases are explicit and visible | | Hundreds of strategies with inconsistent quality | Small, auditable codebase with built-in strategies |

vs better-auth

better-auth is feature-rich but opinionated:

| better-auth | Handshake Auth | |-------------|----------------| | Requires database connection | No database connection - you provide callbacks | | Creates and manages auth tables | You own your schema | | All-in-one (sessions, email verification, etc.) | Auth only - does one thing well | | Great for rapid prototyping | Full control over persistence |

The Sweet Spot

If you want Passport's flexibility with modern ergonomics, without better-auth's database coupling, Handshake Auth is for you.

Features

  • Storage-agnostic - You provide callbacks, you own your data
  • Express-only - Focused on Express.js with cookie-session
  • TypeScript-first - Full type safety with generics
  • Modern - async/await throughout, ESM-first
  • Strategies included:
    • Password (simple password-only for self-hosted apps)
    • Username/Password (traditional email + password)
    • Magic Link (passwordless via email)
    • OAuth: Google, GitHub, Discord, Microsoft, Twitter/X

Installation

npm install handshake-auth

Basic Usage

import { Handshake, UsernamePasswordStrategy } from 'handshake-auth';

// Define your account type
interface Account {
  id: string;
  email: string;
  passwordHash: string;
}

// Create a Handshake instance with your callbacks
const hs = new Handshake<Account>({
  findAccount: async (email) => {
    // Your database lookup here
    return db.accounts.findByEmail(email);
  },
  verifyPassword: async (account, password) => {
    // Your password verification here (e.g., bcrypt.compare)
    return await bcrypt.compare(password, account.passwordHash);
  },
});

// Register the username-password strategy
hs.use(new UsernamePasswordStrategy());

// Authenticate
const result = await hs.authenticate('username-password', '[email protected]', 'their-password');

if (result.account) {
  // Authentication successful
  console.log('Logged in:', result.account.email);
} else {
  // Authentication failed
  console.log('Error:', result.error);
}

Express Integration

Use with Express and cookie-session for a complete authentication setup:

import express from 'express';
import cookieSession from 'cookie-session';
import {
  Handshake,
  UsernamePasswordStrategy,
  handshakeMiddleware,
  login,
  logout,
  requireAuth,
  requireGuest,
} from 'handshake-auth';

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

// Configure cookie-session
app.use(cookieSession({
  name: 'session',
  keys: [process.env.SESSION_SECRET!],
  maxAge: 24 * 60 * 60 * 1000, // 24 hours
}));

// Set up Handshake
const hs = new Handshake<Account>({
  findAccount: async (email) => db.accounts.findByEmail(email),
  verifyPassword: async (account, password) => bcrypt.compare(password, account.passwordHash),
});
hs.use(new UsernamePasswordStrategy());

// Add middleware (attaches authenticate helper to req)
app.use(handshakeMiddleware(hs));

// Login route
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const result = await hs.authenticate('username-password', email, password);

  if (result.account) {
    login(req, result.account);
    res.json({ success: true });
  } else {
    res.status(401).json({ error: result.error });
  }
});

// Logout route
app.post('/logout', (req, res) => {
  logout(req);
  res.json({ success: true });
});

// Protected route (API style - returns 401 JSON if not authenticated)
app.get('/api/me', requireAuth(), (req, res) => {
  res.json({ accountId: req.session?.accountId });
});

// Protected route (Web style - redirects to /login if not authenticated)
app.get('/dashboard', requireAuth({ redirectTo: '/login' }), (req, res) => {
  res.send('Welcome to your dashboard!');
});

// Guest-only route (redirects to /dashboard if already logged in)
app.get('/login', requireGuest({ redirectTo: '/dashboard' }), (req, res) => {
  res.send('<form>...</form>');
});

Magic Link Authentication

Passwordless authentication via email:

import { Handshake, useMagicLink, login } from 'handshake-auth';

// In-memory token storage (use a database in production)
const tokens = new Map<string, { email: string; expiresAt: Date }>();

const hs = new Handshake<Account>({
  findAccount: async (email) => db.accounts.findByEmail(email),
  storeMagicToken: async (email, token, expiresAt) => {
    tokens.set(token, { email, expiresAt });
  },
  verifyMagicToken: async (token) => {
    const record = tokens.get(token);
    if (!record || record.expiresAt < new Date()) return null;
    tokens.delete(token); // One-time use
    return { email: record.email };
  },
});

// Register magic link strategy
useMagicLink(hs, {
  baseUrl: 'http://localhost:3000',
  sendMagicLink: async (email, token, url) => {
    // Send email with the magic link URL
    console.log(`Magic link for ${email}: ${url}`);
  },
});

// Request magic link
app.post('/auth/magic', async (req, res) => {
  const result = await hs.authenticate('magic:send', req.body.email);
  if (!result.error) {
    res.json({ message: 'Check your email!' });
  } else {
    res.status(400).json({ error: result.error });
  }
});

// Verify magic link
app.get('/auth/magic/callback', async (req, res) => {
  const result = await hs.authenticate('magic:verify', req.query.token);
  if (result.account) {
    login(req, result.account);
    res.redirect('/dashboard');
  } else {
    res.status(401).send('Invalid or expired link');
  }
});

OAuth Authentication

Handshake Auth supports OAuth providers with a simple, consistent API:

import {
  Handshake,
  GoogleStrategy,
  GitHubStrategy,
  DiscordStrategy,
  MicrosoftStrategy,
  TwitterXStrategy,
  login,
} from 'handshake-auth';

const hs = new Handshake<Account>({
  findAccount: async (email) => db.accounts.findByEmail(email),
  findOrCreateFromOAuth: async (provider, profile) => {
    // Find existing account or create new one
    let account = await db.accounts.findByProviderId(provider, profile.id);
    if (!account) {
      account = await db.accounts.create({
        email: profile.email,
        name: profile.name,
        provider,
        providerId: profile.id,
      });
    }
    return account;
  },
});

// Register OAuth strategies (only the ones you need)
if (process.env.GOOGLE_CLIENT_ID) {
  hs.use(new GoogleStrategy({
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    redirectUri: 'http://localhost:3000/auth/google/callback',
  }));
}

if (process.env.GITHUB_CLIENT_ID) {
  hs.use(new GitHubStrategy({
    clientId: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    redirectUri: 'http://localhost:3000/auth/github/callback',
  }));
}

// Generic OAuth routes
app.get('/auth/:provider', async (req, res) => {
  const result = await hs.authenticate(req.params.provider, req, res, 'redirect');
  if ('redirectUrl' in result) {
    res.redirect(result.redirectUrl);
  } else {
    res.status(400).send(result.error);
  }
});

app.get('/auth/:provider/callback', async (req, res) => {
  const result = await hs.authenticate(req.params.provider, req, res, 'callback');
  if (result.account) {
    login(req, result.account);
    res.redirect('/dashboard');
  } else {
    res.status(401).send(result.error);
  }
});

Supported OAuth Providers

| Provider | Strategy | Notes | |----------|----------|-------| | Google | GoogleStrategy | OpenID Connect | | GitHub | GitHubStrategy | Fetches email from /user/emails if needed | | Discord | DiscordStrategy | Standard OAuth2 | | Microsoft | MicrosoftStrategy | Supports tenant configuration | | Twitter/X | TwitterXStrategy | OAuth 2.0 with PKCE (no email provided) |

Documentation

For detailed documentation, see the docs/ folder:

Example Application

See the examples/express-app directory for a complete working example with all authentication strategies.

cd examples/express-app
npm install
cp .env.example .env
npm run dev

What This Library Does NOT Do

  • No account management - No account model, no persistence, no database integrations
  • No session management - Uses cookie-session but doesn't manage server-side sessions
  • No route ownership - No automatic /login, /callback, or /logout routes
  • No email sending - Magic links require user-supplied delivery
  • No UI - No forms, no opinionated UX

ChangeLog

  • 0.2.0 - All strategies implemented (Password, Username/Password, Magic Link, Google, GitHub, Discord, Microsoft, Twitter/X), Express middleware with route guards, comprehensive documentation
  • 0.1.0 - Initial development release

License

ISC