Database-agnostic Express authentication middleware for PostgreSQL
npm install @eaccess/authAn 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.
- 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
``bash`
npm install @prsm/easy-auth express-session
`typescript
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(),
});
});
`
Easy-auth supports OAuth providers (GitHub, Google, Azure) with a clean, extensible API.
`typescript
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));
`
`typescript
// 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');
}
});
`
`html`
Login with GitHub
Login with Google
Login with Azure
1. User clicks "Login with GitHub" → Browser goes to /auth/github/auth/github/callback?code=abc123
2. Server redirects to GitHub → User sees GitHub's login page
3. User authorizes your app → GitHub redirects to handleCallback()
4. Server processes callback → does:createUser()
- 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 , create account + provider record, log them in
`typescript`
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');
}
}
});
Create a .env file:
`bashGitHub OAuth App (https://github.com/settings/developers)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
$3
For more control over the OAuth flow:
`typescript
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.
$3
Enable MFA in your auth config:
`typescript
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
}
};
`$3
When MFA is enabled, the login process becomes:
`typescript
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 });
}
});
`$3
The
SecondFactorRequiredError.challenge contains:`typescript
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:
1. Send the otpValue codes via your email/SMS service
2. Remove both otpValue and selectors from client responses for security
3. Only return the maskedContact to the frontend (selectors are automatically stored in the user's session)$3
After receiving
SecondFactorRequiredError, verify the second factor:`typescript
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 });
}
});
`$3
Users can enroll in multiple MFA methods:
#### TOTP (Authenticator App)
`typescript
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
`typescript
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
`typescript
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 });
}
});
`$3
For production apps, require verification during enrollment:
`typescript
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 });
}
});
`$3
`typescript
// 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
$3
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:
`typescript
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:`typescript
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.`typescript
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:`typescript
declare module "express-session" {
interface SessionData {
userId?: string;
}
}
`$3
`typescript
interface AuthConfig {
// PostgreSQL connection pool
db: Pool;
// Optional OAuth new user creation function
createUser?: (userData: OAuthUserData) => string | number | Promise; // 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:
`sql
-- 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_log
`API Reference
$3
#### Authentication
-
isLoggedIn(): boolean
- login(email, password, remember?): Promise
- completeTwoFactorLogin(): Promise
- logout(): Promise
- register(email, password, callback?): Promise#### User Info
-
getId(): number | null
- getEmail(): string | null
- getStatus(): number | null
- getVerified(): boolean | null
- getRoleNames(rolemask?): string[]
- getStatusName(): string | null#### Permissions
-
hasRole(role): Promise
- isAdmin(): Promise
- isRemembered(): boolean#### Email Management
-
changeEmail(newEmail, callback): Promise
- confirmEmail(token): Promise
- confirmEmailAndLogin(token, remember?): Promise#### Password Management
-
resetPassword(email, expiresAfter?, maxRequests?, callback?): Promise
- confirmResetPassword(token, password, logout?): Promise
- verifyPassword(password): Promise#### Session Management
-
logoutEverywhere(): Promise
- logoutEverywhereElse(): Promise#### Multi-Factor Authentication (
req.auth.twoFactor)
- isEnabled(): Promise
- totpEnabled(): Promise
- emailEnabled(): Promise
- smsEnabled(): Promise
- getEnabledMethods(): PromiseSetup Methods:
-
setup.totp(requireVerification?): Promise
- setup.email(email?, requireVerification?): Promise
- setup.sms(phone, requireVerification?): PromiseCompletion Methods (for verification during enrollment):
-
complete.totp(code): Promise
- complete.email(code): Promise
- complete.sms(code): PromiseVerification Methods (during login):
-
verify.totp(code): Promise
- verify.email(code): Promise
- verify.sms(code): Promise
- verify.backupCode(code): Promise
- verify.otp(code): PromiseManagement Methods:
-
disable(mechanism): Promise
- generateNewBackupCodes(): Promise
- getContact(mechanism): Promise$3
#### User Management
-
createUser(credentials, callback?): Promise
- loginAsUserBy(identifier): Promise
- deleteUserBy(identifier): Promise#### Role Management
-
addRoleForUserBy(identifier, role): Promise
- removeRoleForUserBy(identifier, role): Promise
- hasRoleForUserBy(identifier, role): Promise#### Account Management
-
changePasswordForUserBy(identifier, password): Promise
- setStatusForUserBy(identifier, status): Promise
- initiatePasswordResetForUserBy(identifier, expiresAfter?, callback?): Promise$3
`typescript
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
`typescript
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 more
`Error Handling
`typescript
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
$3
`typescript
import { Pool } from 'pg';const pool = new Pool({
connectionString: 'postgresql://user:password@localhost:5432/dbname'
});
const config = {
db: pool,
tablePrefix: 'auth_',
};
`$3
`typescript
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: 'user@example.com' },
AuthRole.Admin | AuthRole.Editor
);
``MIT