Local Auth

Authentication LibraryNode.jsJavaScriptLocal AuthenticationSession ManagementPassword Authentication

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

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
    });
});