Local Auth
認証ライブラリ
Local Auth
概要
Local Authは、Node.jsアプリケーション向けの軽量でシンプルなローカル認証ライブラリで、Passport.jsの代替として、最小限の依存関係でユーザー名・パスワード認証を実装します。
詳細
Local Authは、現代のNode.jsアプリケーションにおいて、複雑なPassport.jsに代わる軽量な認証ソリューションとして注目されています。従来のPassport.jsが多機能である一方で設定が複雑になりがちな課題を解決し、シンプルで理解しやすいAPIを提供します。主な特徴として、ユーザー名・パスワードベースの認証、セッション管理、CSRF対策、bcryptによるパスワードハッシュ化、レート制限機能、二要素認証サポートなどがあります。Express.js、Koa.js、Fastifyなどの主要なNode.jsフレームワークとの統合が容易で、既存のアプリケーションへの導入もスムーズです。データベースアクセスは抽象化されており、MongoDB、PostgreSQL、MySQL、SQLiteなど様々なデータストアに対応可能です。また、JWT(JSON Web Token)との組み合わせによるステートレス認証や、サーバーサイドセッションとクライアントサイドトークンのハイブリッド認証も実装できます。軽量性を重視しながらも、企業レベルのセキュリティ要件を満たす堅牢な認証機能を提供します。
メリット・デメリット
メリット
- 軽量設計: 最小限の依存関係で高いパフォーマンス
- シンプルなAPI: 直感的で理解しやすい認証フロー
- カスタマイズ性: 柔軟な設定とカスタム認証ロジック対応
- フレームワーク非依存: Express、Koa、Fastify等幅広く対応
- セキュリティ重視: bcrypt、CSRF対策、レート制限などの標準実装
- TypeScript対応: 型安全性を重視した開発体験
- デバッグ容易: シンプルな構造でトラブルシューティングが簡単
デメリット
- 機能限定: Passport.jsと比較して対応認証方式が限定的
- コミュニティ: Passport.jsと比較してコミュニティサイズが小さい
- ドキュメント: 情報量がPassport.jsほど豊富ではない
- ソーシャル認証: OAuth2やOpenID Connectは別途実装が必要
- プラグイン: 拡張機能のエコシステムが限定的
主要リンク
- Node.js認証ベストプラクティス
- bcrypt.js公式ドキュメント
- Express Session
- Node.js Security Checklist
- OWASP Authentication Cheat Sheet
- JWT.io
書き方の例
Hello World(基本的なローカル認証)
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分
this.loginAttempts = new Map();
}
// パスワードのハッシュ化
async hashPassword(password) {
return await bcrypt.hash(password, this.hashRounds);
}
// パスワードの検証
async verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// レート制限チェック
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 {
// ロックアウト時間経過、リセット
this.loginAttempts.delete(identifier);
return true;
}
}
return true;
}
// 失敗試行の記録
recordFailedAttempt(identifier) {
const attempts = this.loginAttempts.get(identifier) || { count: 0, lastAttempt: 0 };
attempts.count += 1;
attempts.lastAttempt = Date.now();
this.loginAttempts.set(identifier, attempts);
}
// 成功時の試行リセット
resetFailedAttempts(identifier) {
this.loginAttempts.delete(identifier);
}
// ユーザー認証
async authenticate(username, password, request) {
const clientIP = request.ip || request.connection.remoteAddress;
const identifier = `${username}:${clientIP}`;
// レート制限チェック
if (!this.checkRateLimit(identifier)) {
throw new Error('Too many login attempts. Please try again later.');
}
try {
// ユーザー取得
const user = await this.userProvider.findByUsername(username);
if (!user) {
this.recordFailedAttempt(identifier);
throw new Error('Invalid username or password');
}
// パスワード検証
const isValid = await this.verifyPassword(password, user.passwordHash);
if (!isValid) {
this.recordFailedAttempt(identifier);
throw new Error('Invalid username or password');
}
// 成功時の処理
this.resetFailedAttempts(identifier);
return {
id: user.id,
username: user.username,
email: user.email,
roles: user.roles || []
};
} catch (error) {
this.recordFailedAttempt(identifier);
throw error;
}
}
// セッション設定ミドルウェア
sessionMiddleware() {
return session({
secret: this.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24時間
}
});
}
// 認証チェックミドルウェア
requireAuth() {
return (req, res, next) => {
if (req.session && req.session.user) {
return next();
} else {
return res.status(401).json({ error: 'Authentication required' });
}
};
}
}
// 使用例
const app = express();
app.use(express.json());
// ユーザープロバイダー(実際の実装ではDBを使用)
const userProvider = {
users: new Map(),
async findByUsername(username) {
return this.users.get(username);
},
async createUser(userData) {
this.users.set(userData.username, userData);
return userData;
}
};
const auth = new LocalAuth({
userProvider,
sessionSecret: 'your-super-secret-key'
});
app.use(auth.sessionMiddleware());
// ユーザー登録
app.post('/register', async (req, res) => {
try {
const { username, password, email } = req.body;
if (await userProvider.findByUsername(username)) {
return res.status(400).json({ error: 'Username already exists' });
}
const passwordHash = await auth.hashPassword(password);
const user = await userProvider.createUser({
id: Date.now().toString(),
username,
email,
passwordHash,
roles: ['user']
});
res.json({ message: 'User created successfully', userId: user.id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ログイン
app.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await auth.authenticate(username, password, req);
req.session.user = user;
res.json({ message: 'Login successful', user });
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// ログアウト
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' });
});
});
// 保護されたルート
app.get('/profile', auth.requireAuth(), (req, res) => {
res.json({ user: req.session.user });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
JWT統合とステートレス認証
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 Set(); // 実際の実装ではDBに保存
}
// JWTトークンの生成
generateTokens(user) {
const payload = {
id: user.id,
username: user.username,
roles: user.roles
};
const accessToken = jwt.sign(payload, this.jwtSecret, {
expiresIn: this.jwtExpiresIn,
issuer: 'local-auth',
audience: 'api'
});
const refreshToken = jwt.sign(
{ id: user.id, type: 'refresh' },
this.jwtSecret,
{ expiresIn: this.refreshTokenExpiresIn }
);
this.refreshTokens.add(refreshToken);
return { accessToken, refreshToken };
}
// JWTトークンの検証
verifyToken(token) {
try {
return jwt.verify(token, this.jwtSecret);
} catch (error) {
throw new Error('Invalid or expired token');
}
}
// リフレッシュトークンの検証と新しいトークンの生成
async refreshTokens(refreshToken) {
if (!this.refreshTokens.has(refreshToken)) {
throw new Error('Invalid refresh token');
}
try {
const decoded = jwt.verify(refreshToken, this.jwtSecret);
if (decoded.type !== 'refresh') {
throw new Error('Invalid token type');
}
// 古いリフレッシュトークンを削除
this.refreshTokens.delete(refreshToken);
// ユーザー情報を取得
const user = await this.userProvider.findById(decoded.id);
if (!user) {
throw new Error('User not found');
}
// 新しいトークンを生成
return this.generateTokens(user);
} catch (error) {
this.refreshTokens.delete(refreshToken);
throw new Error('Invalid refresh token');
}
}
// JWT認証ミドルウェア
jwtAuth() {
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);
try {
const decoded = this.verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: error.message });
}
};
}
// ロール基盤アクセス制御
requireRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userRoles = req.user.roles || [];
const hasRole = roles.some(role => userRoles.includes(role));
if (!hasRole) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
}
// JWT認証実装例
const jwtAuth = new JWTLocalAuth({
userProvider,
jwtSecret: process.env.JWT_SECRET,
jwtExpiresIn: '15m',
refreshTokenExpiresIn: '7d'
});
// JWTログイン
app.post('/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await jwtAuth.authenticate(username, password, req);
const tokens = jwtAuth.generateTokens(user);
res.json({
message: 'Login successful',
user: {
id: user.id,
username: user.username,
roles: user.roles
},
...tokens
});
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// トークンリフレッシュ
app.post('/auth/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
const tokens = await jwtAuth.refreshTokens(refreshToken);
res.json(tokens);
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// JWTで保護されたルート
app.get('/api/profile', jwtAuth.jwtAuth(), (req, res) => {
res.json({ user: req.user });
});
// 管理者専用ルート
app.get('/api/admin',
jwtAuth.jwtAuth(),
jwtAuth.requireRole(['admin']),
(req, res) => {
res.json({ message: 'Admin area accessed' });
}
);
二要素認証(2FA)実装
const speakeasy = require('speakeasy');
const qrcode = require('qrcode');
class TwoFactorAuth extends JWTLocalAuth {
constructor(options = {}) {
super(options);
this.appName = options.appName || 'Local Auth App';
}
// TOTP秘密鍵の生成
generateTOTPSecret(user) {
const secret = speakeasy.generateSecret({
name: `${this.appName} (${user.username})`,
issuer: this.appName,
length: 32
});
return {
secret: secret.base32,
otpauthUrl: secret.otpauth_url
};
}
// QRコードの生成
async generateQRCode(otpauthUrl) {
try {
return await qrcode.toDataURL(otpauthUrl);
} catch (error) {
throw new Error('Failed to generate QR code');
}
}
// TOTPトークンの検証
verifyTOTP(token, secret) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 2 // 前後2つの時間窓を許容
});
}
// 2FA有効化
async enable2FA(userId) {
const user = await this.userProvider.findById(userId);
if (!user) {
throw new Error('User not found');
}
const totpData = this.generateTOTPSecret(user);
// 秘密鍵を一時保存(検証後に永続化)
user.tempTotpSecret = totpData.secret;
await this.userProvider.updateUser(user);
const qrCodeUrl = await this.generateQRCode(totpData.otpauthUrl);
return {
secret: totpData.secret,
qrCode: qrCodeUrl,
manualEntryKey: totpData.secret
};
}
// 2FA設定の確認
async confirm2FA(userId, token) {
const user = await this.userProvider.findById(userId);
if (!user || !user.tempTotpSecret) {
throw new Error('2FA setup not initiated');
}
const isValid = this.verifyTOTP(token, user.tempTotpSecret);
if (!isValid) {
throw new Error('Invalid verification code');
}
// 2FAを有効化
user.totpSecret = user.tempTotpSecret;
user.twoFactorEnabled = true;
delete user.tempTotpSecret;
await this.userProvider.updateUser(user);
return { message: '2FA enabled successfully' };
}
// 2FA無効化
async disable2FA(userId, token) {
const user = await this.userProvider.findById(userId);
if (!user || !user.twoFactorEnabled) {
throw new Error('2FA not enabled');
}
const isValid = this.verifyTOTP(token, user.totpSecret);
if (!isValid) {
throw new Error('Invalid verification code');
}
user.twoFactorEnabled = false;
delete user.totpSecret;
await this.userProvider.updateUser(user);
return { message: '2FA disabled successfully' };
}
// 2FA付きログイン
async authenticateWith2FA(username, password, totpToken, request) {
// 通常の認証
const user = await this.authenticate(username, password, request);
// 2FAが有効な場合は検証
if (user.twoFactorEnabled) {
if (!totpToken) {
throw new Error('TOTP token required');
}
const fullUser = await this.userProvider.findById(user.id);
const isValidTOTP = this.verifyTOTP(totpToken, fullUser.totpSecret);
if (!isValidTOTP) {
throw new Error('Invalid TOTP token');
}
}
return user;
}
}
// 2FA実装例
const twoFactorAuth = new TwoFactorAuth({
userProvider,
appName: 'Secure App'
});
// 2FA設定開始
app.post('/auth/2fa/setup', jwtAuth.jwtAuth(), async (req, res) => {
try {
const userId = req.user.id;
const setupData = await twoFactorAuth.enable2FA(userId);
res.json({
message: '2FA setup initiated',
qrCode: setupData.qrCode,
manualEntryKey: setupData.secret
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 2FA設定確認
app.post('/auth/2fa/confirm', jwtAuth.jwtAuth(), async (req, res) => {
try {
const { token } = req.body;
const userId = req.user.id;
const result = await twoFactorAuth.confirm2FA(userId, token);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 2FA付きログイン
app.post('/auth/login-2fa', async (req, res) => {
try {
const { username, password, totpToken } = req.body;
const user = await twoFactorAuth.authenticateWith2FA(
username,
password,
totpToken,
req
);
const tokens = twoFactorAuth.generateTokens(user);
res.json({
message: 'Login successful',
user: {
id: user.id,
username: user.username,
roles: user.roles,
twoFactorEnabled: user.twoFactorEnabled
},
...tokens
});
} catch (error) {
res.status(401).json({ error: error.message });
}
});
データベース統合とパスワードリセット
const nodemailer = require('nodemailer');
const crypto = require('crypto');
class DatabaseLocalAuth extends TwoFactorAuth {
constructor(options = {}) {
super(options);
this.emailTransporter = options.emailTransporter;
this.resetTokenExpiry = options.resetTokenExpiry || 3600000; // 1時間
this.pendingResets = new Map(); // 実際の実装ではDBに保存
}
// パスワードリセットトークンの生成
generateResetToken() {
return crypto.randomBytes(32).toString('hex');
}
// パスワードリセット要求
async requestPasswordReset(email) {
const user = await this.userProvider.findByEmail(email);
if (!user) {
// セキュリティ上、ユーザーの存在を明かさない
return { message: 'If email exists, reset link has been sent' };
}
const resetToken = this.generateResetToken();
const expiresAt = Date.now() + this.resetTokenExpiry;
// リセットトークンを保存
this.pendingResets.set(resetToken, {
userId: user.id,
email: user.email,
expiresAt
});
// メール送信
await this.sendResetEmail(user.email, resetToken);
return { message: 'If email exists, reset link has been sent' };
}
// パスワードリセット実行
async resetPassword(resetToken, newPassword) {
const resetData = this.pendingResets.get(resetToken);
if (!resetData || Date.now() > resetData.expiresAt) {
throw new Error('Invalid or expired reset token');
}
const user = await this.userProvider.findById(resetData.userId);
if (!user) {
throw new Error('User not found');
}
// 新しいパスワードをハッシュ化
const passwordHash = await this.hashPassword(newPassword);
// パスワード更新
user.passwordHash = passwordHash;
await this.userProvider.updateUser(user);
// リセットトークンを削除
this.pendingResets.delete(resetToken);
// すべてのセッション/トークンを無効化
await this.revokeAllUserTokens(user.id);
return { message: 'Password reset successful' };
}
// パスワードリセットメール送信
async sendResetEmail(email, resetToken) {
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
const mailOptions = {
from: process.env.FROM_EMAIL,
to: email,
subject: 'Password Reset Request',
html: `
<h2>Password Reset Request</h2>
<p>You requested a password reset. Click the link below to reset your password:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
`
};
if (this.emailTransporter) {
await this.emailTransporter.sendMail(mailOptions);
} else {
console.log('Reset email would be sent:', mailOptions);
}
}
// ユーザーの全トークンを無効化
async revokeAllUserTokens(userId) {
// JWTトークンの無効化(ブラックリスト方式)
// 実際の実装ではDBやRedisにブラックリストを保存
console.log(`All tokens revoked for user: ${userId}`);
}
// パスワード変更
async changePassword(userId, currentPassword, newPassword) {
const user = await this.userProvider.findById(userId);
if (!user) {
throw new Error('User not found');
}
// 現在のパスワードを検証
const isCurrentValid = await this.verifyPassword(currentPassword, user.passwordHash);
if (!isCurrentValid) {
throw new Error('Current password is incorrect');
}
// 新しいパスワードをハッシュ化
const newPasswordHash = await this.hashPassword(newPassword);
// パスワード更新
user.passwordHash = newPasswordHash;
await this.userProvider.updateUser(user);
return { message: 'Password changed successfully' };
}
// アカウントロック機能
async lockAccount(userId, reason) {
const user = await this.userProvider.findById(userId);
if (!user) {
throw new Error('User not found');
}
user.isLocked = true;
user.lockReason = reason;
user.lockedAt = new Date();
await this.userProvider.updateUser(user);
// 全トークンを無効化
await this.revokeAllUserTokens(userId);
return { message: 'Account locked successfully' };
}
// アカウントロック解除
async unlockAccount(userId) {
const user = await this.userProvider.findById(userId);
if (!user) {
throw new Error('User not found');
}
user.isLocked = false;
delete user.lockReason;
delete user.lockedAt;
await this.userProvider.updateUser(user);
return { message: 'Account unlocked successfully' };
}
// 拡張認証(アカウントロックチェック付き)
async authenticate(username, password, request) {
const user = await super.authenticate(username, password, request);
// アカウントロックチェック
if (user.isLocked) {
throw new Error('Account is locked. Please contact administrator.');
}
return user;
}
}
// データベース統合例
const dbAuth = new DatabaseLocalAuth({
userProvider,
emailTransporter: nodemailer.createTransporter({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
})
});
// パスワードリセット要求
app.post('/auth/forgot-password', async (req, res) => {
try {
const { email } = req.body;
const result = await dbAuth.requestPasswordReset(email);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// パスワードリセット実行
app.post('/auth/reset-password', async (req, res) => {
try {
const { token, newPassword } = req.body;
const result = await dbAuth.resetPassword(token, newPassword);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// パスワード変更
app.post('/auth/change-password', jwtAuth.jwtAuth(), async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
const userId = req.user.id;
const result = await dbAuth.changePassword(userId, currentPassword, newPassword);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 管理者によるアカウントロック
app.post('/admin/lock-account',
jwtAuth.jwtAuth(),
jwtAuth.requireRole(['admin']),
async (req, res) => {
try {
const { userId, reason } = req.body;
const result = await dbAuth.lockAccount(userId, reason);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
);
セキュリティ監査とログ機能
const winston = require('winston');
class AuditableLocalAuth extends DatabaseLocalAuth {
constructor(options = {}) {
super(options);
this.logger = this.setupLogger(options.logConfig);
this.auditEvents = [];
}
setupLogger(config = {}) {
return winston.createLogger({
level: config.level || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: config.errorLog || 'auth-error.log',
level: 'error'
}),
new winston.transports.File({
filename: config.auditLog || 'auth-audit.log'
}),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
}
// 監査イベントの記録
auditLog(event, data = {}) {
const auditEntry = {
timestamp: new Date().toISOString(),
event,
...data
};
this.logger.info('AUDIT', auditEntry);
this.auditEvents.push(auditEntry);
// 重要なセキュリティイベントの場合はアラート
if (this.isCriticalEvent(event)) {
this.sendSecurityAlert(auditEntry);
}
}
isCriticalEvent(event) {
const criticalEvents = [
'BRUTE_FORCE_DETECTED',
'ACCOUNT_LOCKED',
'SUSPICIOUS_LOGIN',
'PASSWORD_RESET_ABUSE',
'TOKEN_THEFT_SUSPECTED'
];
return criticalEvents.includes(event);
}
// セキュリティアラート送信
async sendSecurityAlert(auditEntry) {
// 実際の実装では、Slack、メール、SIEM等に送信
console.log('🚨 SECURITY ALERT:', auditEntry);
}
// 拡張認証(監査ログ付き)
async authenticate(username, password, request) {
const clientIP = request.ip || request.connection.remoteAddress;
const userAgent = request.get('User-Agent');
try {
const user = await super.authenticate(username, password, request);
this.auditLog('LOGIN_SUCCESS', {
userId: user.id,
username: user.username,
clientIP,
userAgent
});
return user;
} catch (error) {
this.auditLog('LOGIN_FAILED', {
username,
reason: error.message,
clientIP,
userAgent
});
// ブルートフォース攻撃の検出
await this.detectBruteForce(username, clientIP);
throw error;
}
}
// ブルートフォース攻撃検出
async detectBruteForce(username, clientIP) {
const recentFailures = this.auditEvents.filter(event =>
event.event === 'LOGIN_FAILED' &&
(event.username === username || event.clientIP === clientIP) &&
Date.now() - new Date(event.timestamp).getTime() < 15 * 60 * 1000 // 15分以内
);
if (recentFailures.length >= 10) {
this.auditLog('BRUTE_FORCE_DETECTED', {
username,
clientIP,
failureCount: recentFailures.length
});
// 自動的にIPをブロック(実際の実装ではファイアウォール等と連携)
await this.blockIP(clientIP, 'Brute force attack detected');
}
}
// 不審な活動の検出
async detectSuspiciousActivity(user, request) {
const clientIP = request.ip;
const userAgent = request.get('User-Agent');
// 異なる地域からのログイン検出
const recentLogins = this.auditEvents.filter(event =>
event.event === 'LOGIN_SUCCESS' &&
event.userId === user.id &&
Date.now() - new Date(event.timestamp).getTime() < 24 * 60 * 60 * 1000 // 24時間以内
);
const differentIPs = new Set(recentLogins.map(login => login.clientIP));
if (differentIPs.size > 3) {
this.auditLog('SUSPICIOUS_LOGIN', {
userId: user.id,
username: user.username,
clientIP,
reason: 'Multiple IP addresses in 24h',
ipCount: differentIPs.size
});
}
// 異なるUser-Agentからのログイン
const differentAgents = new Set(recentLogins.map(login => login.userAgent));
if (differentAgents.size > 2) {
this.auditLog('SUSPICIOUS_LOGIN', {
userId: user.id,
username: user.username,
clientIP,
userAgent,
reason: 'Multiple user agents',
agentCount: differentAgents.size
});
}
}
// セキュリティレポートの生成
generateSecurityReport(startDate, endDate) {
const filteredEvents = this.auditEvents.filter(event => {
const eventDate = new Date(event.timestamp);
return eventDate >= startDate && eventDate <= endDate;
});
const report = {
period: { start: startDate, end: endDate },
totalEvents: filteredEvents.length,
eventSummary: {},
topFailedUsers: {},
topClientIPs: {},
securityIncidents: []
};
// イベント集計
filteredEvents.forEach(event => {
report.eventSummary[event.event] = (report.eventSummary[event.event] || 0) + 1;
if (event.event === 'LOGIN_FAILED') {
report.topFailedUsers[event.username] = (report.topFailedUsers[event.username] || 0) + 1;
report.topClientIPs[event.clientIP] = (report.topClientIPs[event.clientIP] || 0) + 1;
}
if (this.isCriticalEvent(event.event)) {
report.securityIncidents.push(event);
}
});
return report;
}
// IPブロック機能
async blockIP(ip, reason) {
this.auditLog('IP_BLOCKED', {
clientIP: ip,
reason,
blockedAt: new Date().toISOString()
});
// 実際の実装ではファイアウォールやプロキシと連携
console.log(`IP ${ip} blocked: ${reason}`);
}
}
// 使用例
const auditAuth = new AuditableLocalAuth({
userProvider,
logConfig: {
level: 'info',
auditLog: 'security-audit.log',
errorLog: 'security-error.log'
}
});
// セキュリティレポート取得
app.get('/admin/security-report',
jwtAuth.jwtAuth(),
jwtAuth.requireRole(['admin']),
(req, res) => {
const { startDate, endDate } = req.query;
const start = new Date(startDate || Date.now() - 7 * 24 * 60 * 60 * 1000);
const end = new Date(endDate || Date.now());
const report = auditAuth.generateSecurityReport(start, end);
res.json(report);
}
);
console.log('Auditable Local Auth server ready');