@eaccess/auth
v0.1.13
Published
Database-agnostic Express authentication middleware for PostgreSQL
Maintainers
Readme
@prsm/easy-auth
An Express authentication middleware specifically designed for Postgres that provides complete authentication functionality without being tied to any specific ORM, query builder, or user table structure. Comprehensive auth without overwhelming complexity. A clean separation of concerns -- not conflating authentication with user management.
Features
- Flexible User Mapping: Links to your existing user table structure
- Zero ORM Dependencies: Pure SQL with configurable table prefixes
- Complete Auth Flow: Registration, login, email verification, password reset
- Role-based Permissions: Built-in role system with bitmasks
- Remember Me: Persistent login tokens
- Session Management: Force logout, logout everywhere
- Admin Functions: User management and impersonation
- OAuth Integration: GitHub, Google, Azure providers with extensible architecture
- TypeScript Support: Full type safety
Installation
npm install @prsm/easy-auth express-sessionQuick Start
import express from 'express';
import session from 'express-session';
import { Pool } from 'pg';
import { createAuthMiddleware, createAuthTables } from '@prsm/easy-auth';
const app = express();
const pool = new Pool({ connectionString: 'postgresql://...' });
// Setup session middleware
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false,
}));
// Configure auth middleware
const authConfig = {
db: pool,
tablePrefix: 'auth_', // Creates: auth_accounts, auth_confirmations, etc.
};
// Create auth tables (run once)
await createAuthTables(authConfig);
// Add auth middleware
app.use(createAuthMiddleware(authConfig));
// Now use auth in your routes
app.post('/register', async (req, res) => {
try {
// Option 1: Let the library auto-generate a UUID for the user
const account = await req.auth.register(
req.body.email,
req.body.password,
undefined, // Auto-generates UUID
(token) => {
// Send confirmation email with token
console.log('Confirmation token:', token);
}
);
// Option 2: Link to your existing user system
// const user = await db.insert(users).values({...}).returning();
// const account = await req.auth.register(
// req.body.email,
// req.body.password,
// user.id, // Link to your user
// (token) => {
// console.log('Confirmation token:', token);
// }
// );
res.json({ success: true, account });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password, req.body.remember);
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: error.message });
}
});
app.get('/profile', (req, res) => {
if (!req.auth.isLoggedIn()) {
return res.status(401).json({ error: 'Not logged in' });
}
res.json({
email: req.auth.getEmail(),
status: req.auth.getStatusName(),
roles: req.auth.getRoleNames(),
isAdmin: await req.auth.isAdmin(),
});
});OAuth Setup
Easy-auth supports OAuth providers (GitHub, Google, Azure) with a clean, extensible API.
OAuth Configuration
import express from 'express';
import session from 'express-session';
import { Pool } from 'pg';
import { createAuthMiddleware, createAuthTables, type OAuthUserData } from '@prsm/easy-auth';
const app = express();
const pool = new Pool({ connectionString: 'postgresql://...' });
// Your app's user table (example)
const users: Array<{ id: number; name: string; email: string }> = [];
const authConfig = {
db: pool,
// Optional: OAuth createUser function to handle new user registration
createUser: async (userData: OAuthUserData) => {
// userData contains: { id, email, username?, name?, avatar? }
// Create user in your app's user table
const user = await db.insert(users).values({
name: userData.name || userData.username,
email: userData.email,
}).returning();
return user.id; // Return the new user's ID
},
tablePrefix: 'auth_',
// OAuth provider configuration
providers: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/github/callback'
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/google/callback'
},
azure: {
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
tenantId: process.env.AZURE_TENANT_ID!,
redirectUri: 'http://localhost:3000/auth/azure/callback'
}
}
};
app.use(createAuthMiddleware(authConfig));OAuth Routes
// Initiate OAuth flow
app.get('/auth/github', (req, res) => {
const authUrl = req.auth.providers.github.getAuthUrl();
res.redirect(authUrl);
});
// Handle OAuth callback (this does everything!)
app.get('/auth/github/callback', async (req, res) => {
try {
await req.auth.providers.github.handleCallback(req);
res.redirect('/dashboard'); // Success!
} catch (error) {
if (error.message.includes('already have an account')) {
res.redirect('/login?error=email_taken');
} else {
res.redirect('/login?error=oauth_failed');
}
}
});
// Same pattern for Google and Azure
app.get('/auth/google', (req, res) => {
const authUrl = req.auth.providers.google.getAuthUrl();
res.redirect(authUrl);
});
app.get('/auth/google/callback', async (req, res) => {
try {
await req.auth.providers.google.handleCallback(req);
res.redirect('/dashboard');
} catch (error) {
res.redirect('/login?error=oauth_failed');
}
});Frontend Integration
<!-- Login page -->
<a href="/auth/github" class="oauth-btn">
<img src="/github-icon.svg" /> Login with GitHub
</a>
<a href="/auth/google" class="oauth-btn">
<img src="/google-icon.svg" /> Login with Google
</a>
<a href="/auth/azure" class="oauth-btn">
<img src="/azure-icon.svg" /> Login with Azure
</a>OAuth Flow Explained
- User clicks "Login with GitHub" → Browser goes to
/auth/github - Server redirects to GitHub → User sees GitHub's login page
- User authorizes your app → GitHub redirects to
/auth/github/callback?code=abc123 - Server processes callback →
handleCallback()does:- Exchange code for access token
- Fetch user data from GitHub API
- Check if OAuth user exists (by provider + provider_id)
- If exists: log them in
- If new but email exists: throw error
- If completely new: call
createUser(), create account + provider record, log them in
OAuth Error Handling
app.get('/auth/github/callback', async (req, res) => {
try {
await req.auth.providers.github.handleCallback(req);
res.redirect('/dashboard');
} catch (error) {
if (error.message.includes('already have an account')) {
// Email exists with different login method
res.redirect('/login?error=Please use your existing email/password login');
} else if (error.message.includes('No authorization code')) {
// User cancelled or OAuth flow failed
res.redirect('/login?error=Authorization cancelled');
} else {
// Other OAuth errors
console.error('OAuth error:', error);
res.redirect('/login?error=Login failed, please try again');
}
}
});Environment Variables
Create a .env file:
# GitHub OAuth App (https://github.com/settings/developers)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Google OAuth App (https://console.cloud.google.com/)
GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Azure OAuth App (https://portal.azure.com/)
AZURE_CLIENT_ID=your_azure_client_id
AZURE_CLIENT_SECRET=your_azure_client_secret
AZURE_TENANT_ID=your_azure_tenant_idAdvanced OAuth Usage
For more control over the OAuth flow:
app.get('/auth/github/callback', async (req, res) => {
try {
// Get user data without logging in
const userData = await req.auth.providers.github.getUserData(req);
// Your custom logic here
const existingUser = await findUserByEmail(userData.email);
if (existingUser && !existingUser.allowOAuth) {
throw new Error('OAuth disabled for this account');
}
// Then complete the OAuth flow manually
await req.auth.providers.github.handleCallback(req);
res.json({ success: true, user: userData });
} catch (error) {
res.status(400).json({ error: error.message });
}
});Multi-Factor Authentication (MFA)
Easy-auth supports TOTP (authenticator apps), Email OTP, and SMS OTP for enhanced security.
MFA Configuration
Enable MFA in your auth config:
const authConfig = {
db: pool,
twoFactor: {
enabled: true,
requireForOAuth: false, // Skip MFA for OAuth users (optional)
issuer: 'MyApp', // TOTP issuer name
codeLength: 6, // OTP code length
tokenExpiry: '5m', // OTP expiration
totpWindow: 1, // TOTP time window tolerance
backupCodesCount: 10 // Number of backup codes
}
};MFA Login Flow
When MFA is enabled, the login process becomes:
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password);
res.json({ success: true });
} catch (error) {
if (error instanceof SecondFactorRequiredError) {
// User needs to complete MFA
return res.status(202).json({
requiresTwoFactor: true,
availableMethods: error.challenge,
message: 'Please complete two-factor authentication'
});
}
res.status(401).json({ error: error.message });
}
});MFA Challenge Structure
The SecondFactorRequiredError.challenge contains:
interface TwoFactorChallenge {
totp?: boolean; // TOTP available
email?: {
otpValue: string; // The actual OTP code that should be sent via email
maskedContact: string; // "j***@example.com"
};
sms?: {
otpValue: string; // The actual OTP code that should be sent via SMS
maskedContact: string; // "+1***90"
};
selectors?: {
email?: string; // Internal selector (stored in session & database)
sms?: string; // Internal selector (stored in session & database)
};
}Important: The otpValue fields contain the actual codes that should be delivered to the user. The selectors are internal identifiers used by the library. In production, you should:
- Send the
otpValuecodes via your email/SMS service - Remove both
otpValueandselectorsfrom client responses for security - Only return the
maskedContactto the frontend (selectors are automatically stored in the user's session)
Completing MFA Login
After receiving SecondFactorRequiredError, verify the second factor:
app.post('/verify-2fa', async (req, res) => {
try {
const { code, method } = req.body;
// Verify based on method
switch (method) {
case 'totp':
await req.auth.twoFactor.verify.totp(code);
break;
case 'email':
await req.auth.twoFactor.verify.email(code);
break;
case 'sms':
await req.auth.twoFactor.verify.sms(code);
break;
case 'backup':
await req.auth.twoFactor.verify.backupCode(code);
break;
case 'otp':
// Smart OTP - works for both email and SMS
await req.auth.twoFactor.verify.otp(code);
break;
}
// Complete login
await req.auth.completeTwoFactorLogin();
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});MFA Enrollment
Users can enroll in multiple MFA methods:
TOTP (Authenticator App)
app.post('/setup-totp', async (req, res) => {
try {
const { secret, qrCode, backupCodes } = await req.auth.twoFactor.setup.totp();
// Show QR code to user for scanning with authenticator app
res.json({
secret, // Manual entry secret
qrCode, // QR code URL for scanning
backupCodes // One-time backup codes
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});Email OTP
app.post('/setup-email-2fa', async (req, res) => {
try {
await req.auth.twoFactor.setup.email();
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});SMS OTP
app.post('/setup-sms-2fa', async (req, res) => {
try {
const { phoneNumber } = req.body;
await req.auth.twoFactor.setup.sms(phoneNumber);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});MFA Enrollment with Verification
For production apps, require verification during enrollment:
app.post('/setup-totp', async (req, res) => {
try {
// Setup but require verification
const { secret, qrCode } = await req.auth.twoFactor.setup.totp(true);
res.json({ secret, qrCode, requiresVerification: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/verify-totp-setup', async (req, res) => {
try {
const { code } = req.body;
const backupCodes = await req.auth.twoFactor.complete.totp(code);
res.json({ success: true, backupCodes });
} catch (error) {
res.status(400).json({ error: error.message });
}
});MFA Management
// Check MFA status
app.get('/mfa-status', async (req, res) => {
const status = {
enabled: await req.auth.twoFactor.isEnabled(),
methods: {
totp: await req.auth.twoFactor.totpEnabled(),
email: await req.auth.twoFactor.emailEnabled(),
sms: await req.auth.twoFactor.smsEnabled()
}
};
res.json(status);
});
// Disable MFA method
app.delete('/mfa/:method', async (req, res) => {
try {
const mechanism = req.params.method === 'totp' ? 1 :
req.params.method === 'email' ? 2 : 3;
await req.auth.twoFactor.disable(mechanism);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Generate new backup codes
app.post('/mfa/backup-codes', async (req, res) => {
try {
const backupCodes = await req.auth.twoFactor.generateNewBackupCodes();
res.json({ backupCodes });
} catch (error) {
res.status(400).json({ error: error.message });
}
});Configuration
User ID Mapping
The auth library maintains its own auth tables (accounts, roles, sessions) that can optionally link to your application's user records via a user ID.
Registration now takes an optional userId parameter:
app.post('/register', async (req, res) => {
// Option 1: Let easy-auth auto-generate a UUID (simplest)
const account = await req.auth.register(req.body.email, req.body.password);
// Option 2: Link to your existing user table
const user = await db.insert(users).values({
name: req.body.name,
email: req.body.email
}).returning();
const account = await req.auth.register(req.body.email, req.body.password, user.id);
res.json({ success: true, userId: user.id });
});For OAuth, you can optionally provide a createUser function to handle new OAuth users. This is the ONLY use case for createUser - it's not used for regular registration or admin user creation:
const authConfig = {
db: pool,
// ONLY used for OAuth new user creation
createUser: async (userData: OAuthUserData) => {
// Create user in your app's user table
const user = await db.insert(users).values({
name: userData.name || userData.username,
email: userData.email,
}).returning();
return user.id; // This will be stored as user_id in auth tables
}
}If you don't provide createUser for OAuth, a UUID will be auto-generated - no configuration needed!
For login, simply call req.auth.login(). You don't need to identify the user beforehand because the login method itself does the authentication using the provided credentials.
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password);
} catch (error) {
if (error instanceof UserNotFoundError || error instanceof InvalidPasswordError) {
return res.status(401).json({ error: 'Invalid email or password' });
}
if (error instanceof UserInactiveError) {
return res.status(403).json({ error: 'Account inactive' });
}
throw error;
}
res.json({ success: true });
});Important: If you use req.session.userId, it could be helpful to augment the session type if you're using TypeScript:
declare module "express-session" {
interface SessionData {
userId?: string;
}
}AuthConfig
interface AuthConfig {
// PostgreSQL connection pool
db: Pool;
// Optional OAuth new user creation function
createUser?: (userData: OAuthUserData) => string | number | Promise<string | number>; // Called when OAuth user doesn't exist in your system
// Optional settings
tablePrefix?: string; // default: 'user_'
minPasswordLength?: number; // default: 8
maxPasswordLength?: number; // default: 64
rememberDuration?: string; // default: '30d'
rememberCookieName?: string; // default: 'remember_token'
resyncInterval?: string; // default: '30s'
// OAuth provider configuration
providers?: {
github?: GitHubProviderConfig;
google?: GoogleProviderConfig;
azure?: AzureProviderConfig;
};
// Multi-factor authentication
twoFactor?: {
enabled?: boolean; // default: false
requireForOAuth?: boolean; // default: false
issuer?: string; // default: 'EasyAuth'
codeLength?: number; // default: 6
tokenExpiry?: string; // default: '5m'
totpWindow?: number; // default: 1
backupCodesCount?: number; // default: 10
};
}Database Schema
The library creates its own tables that link to your existing user table:
-- your existing user table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
-- whatever else
);
-- library creates these tables
CREATE TABLE user_accounts (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL, -- links to your users.id or auto-generated UUID
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
verified BOOLEAN DEFAULT FALSE,
status INTEGER DEFAULT 0,
rolemask INTEGER DEFAULT 0,
-- ...
);
-- also: user_confirmations, user_remembers, user_resets, user_providers
-- MFA tables: user_2fa_methods, user_2fa_tokens
-- Activity: user_activity_logAPI Reference
Auth Manager (req.auth)
Authentication
isLoggedIn(): booleanlogin(email, password, remember?): Promise<void>completeTwoFactorLogin(): Promise<void>logout(): Promise<void>register(email, password, callback?): Promise<AuthAccount>
User Info
getId(): number | nullgetEmail(): string | nullgetStatus(): number | nullgetVerified(): boolean | nullgetRoleNames(rolemask?): string[]getStatusName(): string | null
Permissions
hasRole(role): Promise<boolean>isAdmin(): Promise<boolean>isRemembered(): boolean
Email Management
changeEmail(newEmail, callback): Promise<void>confirmEmail(token): Promise<string>confirmEmailAndLogin(token, remember?): Promise<void>
Password Management
resetPassword(email, expiresAfter?, maxRequests?, callback?): Promise<void>confirmResetPassword(token, password, logout?): Promise<void>verifyPassword(password): Promise<boolean>
Session Management
logoutEverywhere(): Promise<void>logoutEverywhereElse(): Promise<void>
Multi-Factor Authentication (req.auth.twoFactor)
isEnabled(): Promise<boolean>totpEnabled(): Promise<boolean>emailEnabled(): Promise<boolean>smsEnabled(): Promise<boolean>getEnabledMethods(): Promise<TwoFactorMechanism[]>
Setup Methods:
setup.totp(requireVerification?): Promise<TwoFactorSetupResult>setup.email(email?, requireVerification?): Promise<void>setup.sms(phone, requireVerification?): Promise<void>
Completion Methods (for verification during enrollment):
complete.totp(code): Promise<string[]>complete.email(code): Promise<void>complete.sms(code): Promise<void>
Verification Methods (during login):
verify.totp(code): Promise<void>verify.email(code): Promise<void>verify.sms(code): Promise<void>verify.backupCode(code): Promise<void>verify.otp(code): Promise<void>
Management Methods:
disable(mechanism): Promise<void>generateNewBackupCodes(): Promise<string[]>getContact(mechanism): Promise<string | null>
Admin Manager (req.authAdmin)
User Management
createUser(credentials, callback?): Promise<AuthAccount>loginAsUserBy(identifier): Promise<void>deleteUserBy(identifier): Promise<void>
Role Management
addRoleForUserBy(identifier, role): Promise<void>removeRoleForUserBy(identifier, role): Promise<void>hasRoleForUserBy(identifier, role): Promise<boolean>
Account Management
changePasswordForUserBy(identifier, password): Promise<void>setStatusForUserBy(identifier, status): Promise<void>initiatePasswordResetForUserBy(identifier, expiresAfter?, callback?): Promise<void>
Schema Utilities
import { createAuthTables, dropAuthTables, cleanupExpiredTokens, getAuthTableStats } from '@prsm/easy-auth';
// Setup tables
await createAuthTables(config);
// Cleanup (useful for cron jobs)
await cleanupExpiredTokens(config);
// Get statistics
const stats = await getAuthTableStats(config);
console.log(`${stats.accounts} accounts, ${stats.expiredRemembers} expired tokens`);
// Remove all auth tables
await dropAuthTables(config);Constants
import { AuthStatus, AuthRole } from '@prsm/easy-auth';
// User statuses
AuthStatus.Normal // 0
AuthStatus.Archived // 1
AuthStatus.Banned // 2
AuthStatus.Locked // 3
AuthStatus.PendingReview // 4
AuthStatus.Suspended // 5
// User roles (bitmask)
AuthRole.Admin // 1
AuthRole.Author // 2
AuthRole.Collaborator // 4
// ... many moreError Handling
import {
EmailTakenError,
InvalidPasswordError,
UserNotFoundError,
SecondFactorRequiredError,
InvalidTwoFactorCodeError
} from '@prsm/easy-auth';
app.post('/register', async (req, res) => {
try {
await req.auth.register(email, password);
} catch (error) {
if (error instanceof EmailTakenError) {
return res.status(409).json({ error: 'Email already exists' });
}
if (error instanceof InvalidPasswordError) {
return res.status(400).json({ error: 'Password too weak' });
}
throw error;
}
});
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password);
res.json({ success: true });
} catch (error) {
if (error instanceof SecondFactorRequiredError) {
return res.status(202).json({
requiresTwoFactor: true,
availableMethods: error.challenge
});
}
if (error instanceof InvalidTwoFactorCodeError) {
return res.status(400).json({ error: 'Invalid verification code' });
}
throw error;
}
});Examples
Database Setup
import { Pool } from 'pg';
const pool = new Pool({
connectionString: 'postgresql://user:password@localhost:5432/dbname'
});
const config = {
db: pool,
tablePrefix: 'auth_',
};Role-Based Access Control
app.get('/admin', async (req, res) => {
if (!req.auth.isLoggedIn()) {
return res.status(401).json({ error: 'Not logged in' });
}
if (!await req.auth.hasRole(AuthRole.Admin)) {
return res.status(403).json({ error: 'Admin access required' });
}
// Admin-only content
});
// Add role to user
await req.authAdmin.addRoleForUserBy(
{ email: '[email protected]' },
AuthRole.Admin | AuthRole.Editor
);License
MIT
