Start a new Node.js project. Add authentication. Here is what your package.json looks like within the hour:
Nine packages. Nine different APIs. Nine different maintainers, update cycles, and potential vulnerability surfaces. And you still have to wire them together yourself with hundreds of lines of glue code.
We think authentication deserves better.
The Passport Problem
Passport.js is the de facto Node.js authentication library, and it has been for a decade. It works. But its plugin architecture means every authentication method is a separate package. Want Google login? Install passport-google-oauth20. Want GitHub? Install passport-github2. Want local password auth? Install passport-local. There are 500+ strategy packages, each with its own configuration signature.
The result is boilerplate-heavy code that looks something like this: configure passport, write a serialize function, write a deserialize function, configure each strategy separately, mount session middleware, mount passport middleware, write the callback routes. Across our projects, we found the average Passport setup required 180-250 lines of configuration code before a single user could log in.
The NextAuth Trap
NextAuth (now Auth.js) simplified things considerably — for Next.js users. The problem? It couples your authentication to your framework. Migrating from Next.js to SvelteKit, Remix, or a plain Express server means rewriting your entire auth layer. Your session management, your callbacks, your provider configuration — all of it is NextAuth-specific.
Authentication is infrastructure. It should not be locked to a UI framework.
The JWT + bcrypt + 2FA Dance
Even if you skip Passport, you still end up juggling:
jsonwebtokenfor token signing and verification — but it has no built-in refresh token rotationbcryptjsfor password hashing — but you have to remember to set the right salt rounds and handle timing attacksspeakeasyorotplibfor TOTP-based 2FA — but you need to generate QR codes separately- A separate RBAC library like
casloraccesscontrolfor permissions — with its own DSL to learn
Each library has its own error handling patterns, its own configuration format, its own testing strategy. Your authentication "system" is actually five loosely coupled libraries that you maintain the integration for.
forge/auth: One Import, One API
import { createAuth } from '@hyperbridge/forge/auth'; const auth = createAuth({ secret: process.env.AUTH_SECRET, providers: { google: { clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET, }, github: { clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }, }, jwt: { expiresIn: '15m', refreshExpiresIn: '7d' }, mfa: { issuer: 'MyApp' }, });
That is the entire configuration. OAuth2 providers, JWT settings, and MFA — all in one object, one import.
Password Hashing with Sensible Defaults
// Hash with auto-tuned cost factor const hash = await auth.hashPassword('user-password'); // Verify with constant-time comparison (no timing attacks) const valid = await auth.verifyPassword('user-password', hash); // Automatic rehashing when cost factor is outdated const { valid, rehash } = await auth.verifyPassword(password, hash, { autoRehash: true }); if (rehash) await updateUserHash(userId, rehash);
JWT with Built-in Refresh Rotation
// Issue access + refresh token pair const tokens = auth.issueTokens({ sub: user.id, role: user.role, permissions: user.permissions, }); // → { accessToken, refreshToken, expiresAt } // Verify and decode const payload = auth.verifyToken(tokens.accessToken); // Rotate refresh token (old one is invalidated) const newTokens = await auth.refreshTokens(tokens.refreshToken);
Multi-Factor Authentication
// Generate TOTP secret + QR code data URL const mfa = auth.generateMFA(user.email); // → { secret, qrCodeDataURL, backupCodes } // Verify a TOTP code from authenticator app const valid = auth.verifyMFA(mfa.secret, '482901'); // Verify a backup code (single-use) const valid = auth.verifyBackupCode(mfa.backupCodes, 'ABCD-1234-EFGH');
Role-Based Access Control
// Define roles and permissions const rbac = auth.defineRoles({ admin: ['users:*', 'posts:*', 'settings:*'], editor: ['posts:read', 'posts:write', 'posts:delete'], viewer: ['posts:read'], }); // Check permissions anywhere if (rbac.can(user.role, 'posts:delete')) { await deletePost(postId); } // Middleware for route protection app.delete('/api/posts/:id', auth.requireAuth(), auth.requirePermission('posts:delete'), deleteHandler );
Magic Links
// Send a magic link (generates signed, time-limited URL) const link = auth.createMagicLink({ email: 'user@example.com', redirectTo: '/dashboard', expiresIn: '15m', }); // → https://myapp.com/auth/verify?token=eyJhbG... // Verify when user clicks const session = await auth.verifyMagicLink(token); // → { user, tokens, redirectTo }
What You Actually Ship
With Passport and friends, your authentication code is scattered across middleware files, strategy configurations, session stores, and route handlers. With forge/auth, your entire auth system is one object with a consistent API.
Compare the dependency surface:
vs.
Zero dependencies means zero supply chain risk. No left-pad moments. No event-stream attacks. No hidden sub-dependency with a critical CVE that you discover in production at 2 AM.
Framework Agnostic by Design
forge/auth works with Express, Fastify, Hono, Koa, or no framework at all. It works in Node.js, Deno, Bun, and Cloudflare Workers. Your auth configuration is portable because it does not depend on any framework's middleware system. Switch from Express to Hono? Your auth code stays the same. Deploy to the edge? It just works — no binary dependencies to worry about.
Authentication is too important to scatter across five packages maintained by five different people with five different release schedules. It should be one coherent system with one API, one set of documentation, and one team responsible for keeping it secure.
HBForge is currently available exclusively for enterprise clients. Opening to the Developer Community on June 25, 2026.
Contact kr@hyperbridge.in for early access.