Local Auth
Authentication Library
Local Auth
Overview
Local Auth is a lightweight and simple local authentication library for Node.js applications, serving as an alternative to Passport.js with minimal dependencies for implementing username/password authentication.
Details
Local Auth has gained attention as a lightweight authentication solution for modern Node.js applications, serving as an alternative to the complex Passport.js. While Passport.js is feature-rich, it tends to become complex in configuration, which Local Auth solves by providing a simple and understandable API. Key features include username/password-based authentication, session management, CSRF protection, bcrypt password hashing, rate limiting functionality, and two-factor authentication support. It integrates easily with major Node.js frameworks such as Express.js, Koa.js, and Fastify, and can be smoothly introduced into existing applications. Database access is abstracted, supporting various data stores including MongoDB, PostgreSQL, MySQL, and SQLite. It also supports stateless authentication through combination with JWT (JSON Web Token) and hybrid authentication combining server-side sessions with client-side tokens. While emphasizing lightweight design, it provides robust authentication functionality that meets enterprise-level security requirements.
Pros and Cons
Pros
- Lightweight Design: High performance with minimal dependencies
- Simple API: Intuitive and easy-to-understand authentication flow
- High Customizability: Flexible configuration and custom authentication logic support
- Framework Independent: Broad compatibility with Express, Koa, Fastify, etc.
- Security Focused: Standard implementation of bcrypt, CSRF protection, rate limiting, etc.
- TypeScript Support: Development experience focused on type safety
- Easy Debugging: Simple structure makes troubleshooting easy
Cons
- Limited Features: More limited authentication methods compared to Passport.js
- Smaller Community: Smaller community size compared to Passport.js
- Documentation: Not as extensive information as Passport.js
- Social Authentication: OAuth2 and OpenID Connect require separate implementation
- Limited Plugins: Limited ecosystem of extensions
Key Links
- Node.js Authentication Best Practices
- bcrypt.js Official Documentation
- Express Session
- Node.js Security Checklist
- OWASP Authentication Cheat Sheet
- JWT.io
Code Examples
Hello World (Basic Local Authentication)
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
class LocalAuth {
constructor(options = {}) {
this.userProvider = options.userProvider;
this.sessionSecret = options.sessionSecret || 'your-secret-key';
this.hashRounds = options.hashRounds || 12;
this.maxLoginAttempts = options.maxLoginAttempts || 5;
this.lockoutTime = options.lockoutTime || 15 * 60 * 1000; // 15 minutes
this.loginAttempts = new Map();
}
// Password hashing
async hashPassword(password) {
return await bcrypt.hash(password, this.hashRounds);
}
// Password verification
async verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Rate limit check
checkRateLimit(identifier) {
const attempts = this.loginAttempts.get(identifier);
if (!attempts) return true;
if (attempts.count >= this.maxLoginAttempts) {
const timeSinceLastAttempt = Date.now() - attempts.lastAttempt;
if (timeSinceLastAttempt < this.lockoutTime) {
return false;
} else {
// Lockout time passed, reset
this.loginAttempts.delete(identifier);
return true;
}
}
return true;
}
// Record failed attempt
recordFailedAttempt(identifier) {
const attempts = this.loginAttempts.get(identifier) || { count: 0, lastAttempt: 0 };
attempts.count += 1;
attempts.lastAttempt = Date.now();
this.loginAttempts.set(identifier, attempts);
}
// Reset failed attempts on success
resetFailedAttempts(identifier) {
this.loginAttempts.delete(identifier);
}
// Main authentication method
async authenticate(username, password, request) {
const clientIP = request.ip || request.connection.remoteAddress;
const identifier = `${username}:${clientIP}`;
// Rate limit check
if (!this.checkRateLimit(identifier)) {
throw new Error('Too many login attempts. Please try again later.');
}
try {
// User retrieval
const user = await this.userProvider.findByUsername(username);
if (!user) {
this.recordFailedAttempt(identifier);
throw new Error('Invalid credentials');
}
// Password verification
const isValid = await this.verifyPassword(password, user.passwordHash);
if (!isValid) {
this.recordFailedAttempt(identifier);
throw new Error('Invalid credentials');
}
// Success - reset failed attempts
this.resetFailedAttempts(identifier);
return {
user: {
id: user.id,
username: user.username,
email: user.email,
roles: user.roles || []
},
authenticated: true
};
} catch (error) {
this.recordFailedAttempt(identifier);
throw error;
}
}
}
// Basic usage example
const app = express();
// Session configuration
app.use(session({
secret: 'your-session-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // Set to true in production with HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Simple user provider (use database in actual implementation)
const userProvider = {
async findByUsername(username) {
// Mock user data
const users = {
'testuser': {
id: 1,
username: 'testuser',
email: '[email protected]',
passwordHash: '$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LeJJCNtTgL2b.JTJC', // "password123"
roles: ['user']
}
};
return users[username] || null;
}
};
const auth = new LocalAuth({ userProvider });
// Login endpoint
app.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
const result = await auth.authenticate(username, password, req);
// Store user information in session
req.session.user = result.user;
req.session.authenticated = true;
res.json({
message: 'Login successful',
user: result.user
});
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// Logout endpoint
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Could not log out' });
}
res.json({ message: 'Logout successful' });
});
});
// Protected route middleware
function requireAuth(req, res, next) {
if (req.session && req.session.authenticated) {
return next();
} else {
return res.status(401).json({ error: 'Authentication required' });
}
}
// Protected route example
app.get('/profile', requireAuth, (req, res) => {
res.json({
message: 'Protected resource accessed',
user: req.session.user
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
JWT Integration and Token-Based Authentication
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class JWTLocalAuth extends LocalAuth {
constructor(options = {}) {
super(options);
this.jwtSecret = options.jwtSecret || crypto.randomBytes(64).toString('hex');
this.jwtExpiresIn = options.jwtExpiresIn || '1h';
this.refreshTokenExpiresIn = options.refreshTokenExpiresIn || '7d';
this.refreshTokens = new Map(); // Use Redis in production
}
// Generate JWT access token
generateAccessToken(user) {
const payload = {
sub: user.id,
username: user.username,
email: user.email,
roles: user.roles,
type: 'access'
};
return jwt.sign(payload, this.jwtSecret, {
expiresIn: this.jwtExpiresIn,
issuer: 'local-auth',
audience: 'api'
});
}
// Generate refresh token
generateRefreshToken(userId) {
const refreshToken = crypto.randomBytes(64).toString('hex');
const expiresAt = new Date();
expiresAt.setTime(expiresAt.getTime() + (7 * 24 * 60 * 60 * 1000)); // 7 days
this.refreshTokens.set(refreshToken, {
userId,
expiresAt,
createdAt: new Date()
});
return refreshToken;
}
// Verify JWT token
verifyAccessToken(token) {
try {
const decoded = jwt.verify(token, this.jwtSecret, {
issuer: 'local-auth',
audience: 'api'
});
if (decoded.type !== 'access') {
throw new Error('Invalid token type');
}
return decoded;
} catch (error) {
throw new Error('Invalid or expired token');
}
}
// Refresh access token
async refreshAccessToken(refreshToken) {
const tokenData = this.refreshTokens.get(refreshToken);
if (!tokenData) {
throw new Error('Invalid refresh token');
}
if (new Date() > tokenData.expiresAt) {
this.refreshTokens.delete(refreshToken);
throw new Error('Refresh token expired');
}
// Get user data
const user = await this.userProvider.findById(tokenData.userId);
if (!user) {
this.refreshTokens.delete(refreshToken);
throw new Error('User not found');
}
// Generate new tokens
const newAccessToken = this.generateAccessToken(user);
const newRefreshToken = this.generateRefreshToken(user.id);
// Remove old refresh token
this.refreshTokens.delete(refreshToken);
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: this.jwtExpiresIn
};
}
// JWT authentication with user data return
async authenticateWithJWT(username, password, request) {
const result = await this.authenticate(username, password, request);
if (result.authenticated) {
const accessToken = this.generateAccessToken(result.user);
const refreshToken = this.generateRefreshToken(result.user.id);
return {
...result,
tokens: {
accessToken,
refreshToken,
tokenType: 'Bearer',
expiresIn: this.jwtExpiresIn
}
};
}
return result;
}
}
// Express.js middleware for JWT authentication
function jwtAuthMiddleware(auth) {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.substring(7); // Remove "Bearer " prefix
try {
const decoded = auth.verifyAccessToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: error.message });
}
};
}
// Usage example
const jwtAuth = new JWTLocalAuth({
userProvider,
jwtSecret: process.env.JWT_SECRET || 'your-jwt-secret-key',
jwtExpiresIn: '15m',
refreshTokenExpiresIn: '7d'
});
// JWT login endpoint
app.post('/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
const result = await jwtAuth.authenticateWithJWT(username, password, req);
res.json({
message: 'Login successful',
user: result.user,
tokens: result.tokens
});
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// Token refresh endpoint
app.post('/auth/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
const result = await jwtAuth.refreshAccessToken(refreshToken);
res.json({
message: 'Token refreshed successfully',
tokens: result
});
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// Protected route with JWT
app.get('/api/protected', jwtAuthMiddleware(jwtAuth), (req, res) => {
res.json({
message: 'Protected resource accessed',
user: {
id: req.user.sub,
username: req.user.username,
email: req.user.email,
roles: req.user.roles
}
});
});
Two-Factor Authentication (2FA) Implementation
const speakeasy = require('speakeasy');
const qrcode = require('qrcode');
class TwoFactorLocalAuth extends JWTLocalAuth {
constructor(options = {}) {
super(options);
this.appName = options.appName || 'Local Auth App';
}
// Generate 2FA secret for user
async generate2FASecret(username) {
const secret = speakeasy.generateSecret({
name: `${this.appName} (${username})`,
issuer: this.appName,
length: 32
});
// Generate QR code
const qrCodeUrl = await qrcode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32,
qrCode: qrCodeUrl,
backupCodes: this.generateBackupCodes()
};
}
// Generate backup codes
generateBackupCodes(count = 10) {
const codes = [];
for (let i = 0; i < count; i++) {
const code = crypto.randomBytes(4).toString('hex').toUpperCase();
codes.push(code);
}
return codes;
}
// Verify 2FA token
verify2FAToken(secret, token) {
return speakeasy.totp.verify({
secret: secret,
token: token,
window: 2, // Allow 2 time steps tolerance
encoding: 'base32'
});
}
// Authenticate with 2FA support
async authenticateWith2FA(username, password, totpToken, request) {
// First step: Basic authentication
const basicResult = await this.authenticate(username, password, request);
if (!basicResult.authenticated) {
return basicResult;
}
// Second step: 2FA verification
const user = basicResult.user;
// Check if user has 2FA enabled
if (user.twoFactorSecret) {
if (!totpToken) {
return {
authenticated: false,
requires2FA: true,
message: '2FA token required'
};
}
const is2FAValid = this.verify2FAToken(user.twoFactorSecret, totpToken);
if (!is2FAValid) {
// Check backup codes as fallback
if (user.backupCodes && user.backupCodes.includes(totpToken.toUpperCase())) {
// Remove used backup code
await this.userProvider.removeBackupCode(user.id, totpToken.toUpperCase());
} else {
throw new Error('Invalid 2FA token');
}
}
}
// Generate JWT tokens
const accessToken = this.generateAccessToken(user);
const refreshToken = this.generateRefreshToken(user.id);
return {
authenticated: true,
user,
tokens: {
accessToken,
refreshToken,
tokenType: 'Bearer',
expiresIn: this.jwtExpiresIn
}
};
}
}
// 2FA setup and login endpoints
const twoFactorAuth = new TwoFactorLocalAuth({
userProvider,
appName: 'My Secure App'
});
// 2FA setup initiation
app.post('/auth/2fa/setup', jwtAuthMiddleware(twoFactorAuth), async (req, res) => {
try {
const userId = req.user.sub;
const username = req.user.username;
const result = await twoFactorAuth.generate2FASecret(username);
// Store secret temporarily (implement proper temporary storage)
req.session.pending2FASecret = result.secret;
res.json({
message: '2FA setup initiated',
qrCode: result.qrCode,
backupCodes: result.backupCodes
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 2FA setup confirmation
app.post('/auth/2fa/confirm', jwtAuthMiddleware(twoFactorAuth), async (req, res) => {
try {
const { token } = req.body;
const userId = req.user.sub;
const pendingSecret = req.session.pending2FASecret;
if (!pendingSecret) {
return res.status(400).json({ error: '2FA setup not initiated' });
}
if (!token) {
return res.status(400).json({ error: '2FA token required' });
}
const isValid = twoFactorAuth.verify2FAToken(pendingSecret, token);
if (!isValid) {
return res.status(400).json({ error: 'Invalid 2FA token' });
}
// Save 2FA secret to user profile
await userProvider.enable2FA(userId, pendingSecret);
delete req.session.pending2FASecret;
res.json({ message: '2FA enabled successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 2FA login endpoint
app.post('/auth/login-2fa', async (req, res) => {
try {
const { username, password, totpToken } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
const result = await twoFactorAuth.authenticateWith2FA(username, password, totpToken, req);
if (result.requires2FA) {
return res.status(202).json({
message: result.message,
requires2FA: true
});
}
res.json({
message: 'Login successful',
user: result.user,
tokens: result.tokens
});
} catch (error) {
res.status(401).json({ error: error.message });
}
});
Database Integration and Advanced Features
const bcrypt = require('bcryptjs');
const { Pool } = require('pg'); // PostgreSQL example
class DatabaseLocalAuth extends TwoFactorLocalAuth {
constructor(options = {}) {
super(options);
this.db = new Pool(options.database);
this.sessionStore = options.sessionStore;
}
// Enhanced user provider with database
setupUserProvider() {
this.userProvider = {
async findByUsername(username) {
const query = `
SELECT id, username, email, password_hash, roles,
two_factor_secret, backup_codes,
failed_attempts, locked_until, last_login
FROM users
WHERE username = $1 AND active = true
`;
const result = await this.db.query(query, [username]);
return result.rows[0] || null;
},
async findById(id) {
const query = `
SELECT id, username, email, password_hash, roles,
two_factor_secret, backup_codes
FROM users
WHERE id = $1 AND active = true
`;
const result = await this.db.query(query, [id]);
return result.rows[0] || null;
},
async createUser(userData) {
const { username, email, password, roles = ['user'] } = userData;
const passwordHash = await bcrypt.hash(password, 12);
const query = `
INSERT INTO users (username, email, password_hash, roles, created_at)
VALUES ($1, $2, $3, $4, NOW())
RETURNING id, username, email, roles
`;
const result = await this.db.query(query, [username, email, passwordHash, roles]);
return result.rows[0];
},
async updateLastLogin(userId) {
const query = `
UPDATE users
SET last_login = NOW(), failed_attempts = 0, locked_until = NULL
WHERE id = $1
`;
await this.db.query(query, [userId]);
},
async recordFailedLogin(userId) {
const query = `
UPDATE users
SET failed_attempts = failed_attempts + 1,
locked_until = CASE
WHEN failed_attempts + 1 >= 5
THEN NOW() + INTERVAL '15 minutes'
ELSE locked_until
END
WHERE id = $1
`;
await this.db.query(query, [userId]);
},
async enable2FA(userId, secret) {
const query = `
UPDATE users
SET two_factor_secret = $2
WHERE id = $1
`;
await this.db.query(query, [userId, secret]);
},
async removeBackupCode(userId, code) {
const query = `
UPDATE users
SET backup_codes = array_remove(backup_codes, $2)
WHERE id = $1
`;
await this.db.query(query, [userId, code]);
}
};
}
// Session-based authentication with database persistence
async authenticateWithSession(username, password, request) {
const result = await this.authenticate(username, password, request);
if (result.authenticated) {
// Update last login
await this.userProvider.updateLastLogin(result.user.id);
// Create secure session
const sessionData = {
userId: result.user.id,
username: result.user.username,
roles: result.user.roles,
loginTime: new Date().toISOString(),
ipAddress: request.ip,
userAgent: request.get('User-Agent')
};
// Store session in database/Redis
if (this.sessionStore) {
await this.sessionStore.create(request.sessionID, sessionData);
}
request.session.user = sessionData;
request.session.authenticated = true;
}
return result;
}
// Password reset functionality
async initiatePasswordReset(email) {
const user = await this.userProvider.findByEmail(email);
if (!user) {
// Don't reveal if user exists
return { message: 'If the email exists, a reset link has been sent' };
}
const resetToken = crypto.randomBytes(32).toString('hex');
const resetExpires = new Date();
resetExpires.setHours(resetExpires.getHours() + 1); // 1 hour expiry
const query = `
UPDATE users
SET reset_token = $1, reset_expires = $2
WHERE id = $3
`;
await this.db.query(query, [resetToken, resetExpires, user.id]);
// Send email (implement email service)
await this.sendPasswordResetEmail(user.email, resetToken);
return { message: 'Password reset email sent' };
}
async resetPassword(token, newPassword) {
const query = `
SELECT id, email, reset_expires
FROM users
WHERE reset_token = $1 AND reset_expires > NOW()
`;
const result = await this.db.query(query, [token]);
const user = result.rows[0];
if (!user) {
throw new Error('Invalid or expired reset token');
}
const passwordHash = await this.hashPassword(newPassword);
const updateQuery = `
UPDATE users
SET password_hash = $1, reset_token = NULL, reset_expires = NULL
WHERE id = $2
`;
await this.db.query(updateQuery, [passwordHash, user.id]);
return { message: 'Password reset successfully' };
}
// Security audit logging
async logSecurityEvent(eventType, userId, details, request) {
const query = `
INSERT INTO security_audit_log
(event_type, user_id, ip_address, user_agent, details, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())
`;
await this.db.query(query, [
eventType,
userId,
request.ip,
request.get('User-Agent'),
JSON.stringify(details)
]);
}
// Account lockout check
async checkAccountLockout(user) {
if (user.locked_until && new Date() < new Date(user.locked_until)) {
const unlockTime = new Date(user.locked_until).toLocaleString();
throw new Error(`Account locked until ${unlockTime}`);
}
return true;
}
}
// Enhanced authentication service usage
const enhancedAuth = new DatabaseLocalAuth({
database: {
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
jwtSecret: process.env.JWT_SECRET,
appName: 'My Secure Application'
});
// Initialize user provider
enhancedAuth.setupUserProvider();
// User registration endpoint
app.post('/auth/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Input validation
if (!username || !email || !password) {
return res.status(400).json({ error: 'All fields are required' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
const user = await enhancedAuth.userProvider.createUser({
username,
email,
password
});
await enhancedAuth.logSecurityEvent('user_registered', user.id, { email }, req);
res.status(201).json({
message: 'User registered successfully',
user: {
id: user.id,
username: user.username,
email: user.email
}
});
} catch (error) {
if (error.code === '23505') { // PostgreSQL unique violation
return res.status(409).json({ error: 'Username or email already exists' });
}
res.status(500).json({ error: error.message });
}
});
// Enhanced login with security logging
app.post('/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
const result = await enhancedAuth.authenticateWithSession(username, password, req);
await enhancedAuth.logSecurityEvent('login_success', result.user.id, {
username: result.user.username
}, req);
res.json({
message: 'Login successful',
user: result.user
});
} catch (error) {
await enhancedAuth.logSecurityEvent('login_failed', null, {
username: req.body.username,
error: error.message
}, req);
res.status(401).json({ error: error.message });
}
});
// Password reset endpoints
app.post('/auth/forgot-password', async (req, res) => {
try {
const { email } = req.body;
const result = await enhancedAuth.initiatePasswordReset(email);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/auth/reset-password', async (req, res) => {
try {
const { token, password } = req.body;
const result = await enhancedAuth.resetPassword(token, password);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Testing and Security Validation
const request = require('supertest');
const { expect } = require('chai');
describe('Local Auth Security Tests', () => {
let app;
let auth;
beforeEach(() => {
// Setup test app and auth instance
app = createTestApp();
auth = new LocalAuth({ userProvider: mockUserProvider });
});
describe('Rate Limiting', () => {
it('should block after maximum failed attempts', async () => {
const credentials = { username: 'testuser', password: 'wrongpassword' };
// Make 5 failed attempts
for (let i = 0; i < 5; i++) {
await request(app)
.post('/login')
.send(credentials)
.expect(401);
}
// 6th attempt should be blocked
const response = await request(app)
.post('/login')
.send(credentials)
.expect(401);
expect(response.body.error).to.include('Too many login attempts');
});
it('should reset attempts after successful login', async () => {
// Make 3 failed attempts
for (let i = 0; i < 3; i++) {
await request(app)
.post('/login')
.send({ username: 'testuser', password: 'wrong' })
.expect(401);
}
// Successful login should reset counter
await request(app)
.post('/login')
.send({ username: 'testuser', password: 'correct' })
.expect(200);
// Should be able to login again immediately
await request(app)
.post('/login')
.send({ username: 'testuser', password: 'correct' })
.expect(200);
});
});
describe('Password Security', () => {
it('should hash passwords with sufficient rounds', async () => {
const password = 'testpassword123';
const hash = await auth.hashPassword(password);
expect(hash).to.not.equal(password);
expect(hash.startsWith('$2a$12$') || hash.startsWith('$2b$12$')).to.be.true;
});
it('should verify passwords correctly', async () => {
const password = 'testpassword123';
const hash = await auth.hashPassword(password);
const isValid = await auth.verifyPassword(password, hash);
const isInvalid = await auth.verifyPassword('wrongpassword', hash);
expect(isValid).to.be.true;
expect(isInvalid).to.be.false;
});
});
describe('JWT Token Security', () => {
it('should generate valid JWT tokens', async () => {
const jwtAuth = new JWTLocalAuth({
userProvider: mockUserProvider,
jwtSecret: 'test-secret-key'
});
const user = { id: 1, username: 'test', email: '[email protected]', roles: ['user'] };
const token = jwtAuth.generateAccessToken(user);
expect(token).to.be.a('string');
expect(token.split('.')).to.have.length(3); // JWT format
});
it('should reject expired tokens', async () => {
const jwtAuth = new JWTLocalAuth({
userProvider: mockUserProvider,
jwtSecret: 'test-secret-key',
jwtExpiresIn: '1ms' // Immediate expiry
});
const user = { id: 1, username: 'test', email: '[email protected]', roles: ['user'] };
const token = jwtAuth.generateAccessToken(user);
// Wait for token to expire
await new Promise(resolve => setTimeout(resolve, 10));
expect(() => jwtAuth.verifyAccessToken(token)).to.throw('Invalid or expired token');
});
});
describe('2FA Security', () => {
it('should generate valid 2FA secrets', async () => {
const twoFactorAuth = new TwoFactorLocalAuth({
userProvider: mockUserProvider,
appName: 'Test App'
});
const result = await twoFactorAuth.generate2FASecret('testuser');
expect(result.secret).to.be.a('string');
expect(result.secret).to.have.length.above(0);
expect(result.qrCode).to.include('data:image/png;base64');
expect(result.backupCodes).to.be.an('array').with.length(10);
});
it('should validate TOTP tokens correctly', () => {
const twoFactorAuth = new TwoFactorLocalAuth({ userProvider: mockUserProvider });
const secret = 'JBSWY3DPEHPK3PXP'; // Base32 test secret
// Generate current TOTP token
const speakeasy = require('speakeasy');
const token = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
const isValid = twoFactorAuth.verify2FAToken(secret, token);
expect(isValid).to.be.true;
});
});
describe('Session Security', () => {
it('should create secure sessions', async () => {
const response = await request(app)
.post('/login')
.send({ username: 'testuser', password: 'correct' })
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies).to.exist;
const sessionCookie = cookies.find(cookie => cookie.startsWith('connect.sid'));
expect(sessionCookie).to.include('HttpOnly');
});
it('should destroy sessions on logout', async () => {
// Login first
const loginResponse = await request(app)
.post('/login')
.send({ username: 'testuser', password: 'correct' })
.expect(200);
const cookies = loginResponse.headers['set-cookie'];
// Logout
await request(app)
.post('/logout')
.set('Cookie', cookies)
.expect(200);
// Try accessing protected route
await request(app)
.get('/profile')
.set('Cookie', cookies)
.expect(401);
});
});
});
// Performance benchmarks
describe('Local Auth Performance', () => {
it('should handle concurrent login requests', async () => {
const concurrency = 50;
const promises = [];
for (let i = 0; i < concurrency; i++) {
promises.push(
request(app)
.post('/login')
.send({ username: 'testuser', password: 'correct' })
);
}
const startTime = Date.now();
const responses = await Promise.all(promises);
const endTime = Date.now();
responses.forEach(response => {
expect(response.status).to.equal(200);
});
const avgResponseTime = (endTime - startTime) / concurrency;
expect(avgResponseTime).to.be.below(100); // Should be under 100ms per request
});
});