Skip to content

認証方式の詳細

認証は、ユーザーが本人であることを確認するプロセスです。適切な認証方式を選択し、実装することで、セキュリティを確保できます。

💡 実際の事例:

2021年、あるSaaSサービスで認証の不備が発見されました:

  • 🔥 問題: パスワードが平文で保存されていた
  • 🔥 結果: データベースが漏洩し、約10万人のアカウントが侵害された
  • 💸 影響:
    • 約5億円の損害賠償
    • サービスの信頼失墜
    • 法的責任が問われた

教訓:

  • ✅ 適切な認証方式の選択と実装が重要
  • パスワードの適切な管理が必須

基本的なパスワード認証:

// 安全なパスワード認証の実装
const bcrypt = require('bcrypt');
class PasswordAuth {
async hashPassword(password) {
// パスワードをハッシュ化(ソルトを自動生成)
const saltRounds = 12; // コストファクター(高いほど安全だが遅い)
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
async verifyPassword(password, hash) {
// パスワードを検証
const isValid = await bcrypt.compare(password, hash);
return isValid;
}
async createUser(email, password) {
// パスワードをハッシュ化して保存
const passwordHash = await this.hashPassword(password);
await db.users.create({
email,
passwordHash, // 平文のパスワードは保存しない
createdAt: new Date(),
});
}
async authenticate(email, password) {
// ユーザーを取得
const user = await db.users.findOne({ email });
if (!user) {
throw new Error('Invalid credentials');
}
// パスワードを検証
const isValid = await this.verifyPassword(password, user.passwordHash);
if (!isValid) {
throw new Error('Invalid credentials');
}
// セッションまたはトークンを生成
const sessionToken = await this.generateSessionToken(user.id);
return sessionToken;
}
}

パスワードの強度チェック:

// パスワードの強度をチェック
class PasswordValidator {
validatePassword(password) {
const errors = [];
// 長さのチェック(12文字以上)
if (password.length < 12) {
errors.push('パスワードは12文字以上である必要があります');
}
// 大文字のチェック
if (!/[A-Z]/.test(password)) {
errors.push('パスワードには大文字を含める必要があります');
}
// 小文字のチェック
if (!/[a-z]/.test(password)) {
errors.push('パスワードには小文字を含める必要があります');
}
// 数字のチェック
if (!/[0-9]/.test(password)) {
errors.push('パスワードには数字を含める必要があります');
}
// 記号のチェック
if (!/[!@#$%^&*]/.test(password)) {
errors.push('パスワードには記号を含める必要があります');
}
// よく使われるパスワードのチェック
const commonPasswords = ['password', '123456', 'qwerty'];
if (commonPasswords.includes(password.toLowerCase())) {
errors.push('よく使われるパスワードは使用できません');
}
return {
valid: errors.length === 0,
errors,
};
}
}

多要素認証の実装:

// 多要素認証(MFA)の実装
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
class MultiFactorAuth {
async setupMFA(userId) {
// TOTP(Time-based One-Time Password)のシークレットを生成
const secret = speakeasy.generateSecret({
name: `MyApp (${userId})`,
issuer: 'MyApp',
});
// QRコードを生成
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// シークレットを一時的に保存(ユーザーが確認できるように)
await db.mfaSecrets.create({
userId,
secret: secret.base32,
verified: false,
createdAt: new Date(),
});
return {
secret: secret.base32,
qrCodeUrl,
};
}
async verifyMFA(userId, token) {
// ユーザーのMFAシークレットを取得
const mfaSecret = await db.mfaSecrets.findOne({
userId,
verified: true,
});
if (!mfaSecret) {
throw new Error('MFA is not set up');
}
// トークンを検証
const verified = speakeasy.totp.verify({
secret: mfaSecret.secret,
encoding: 'base32',
token,
window: 2, // 前後2分のトークンを許容
});
return verified;
}
async authenticateWithMFA(email, password, mfaToken) {
// 1. パスワード認証
const user = await passwordAuth.authenticate(email, password);
// 2. MFA認証
const mfaValid = await this.verifyMFA(user.id, mfaToken);
if (!mfaValid) {
throw new Error('Invalid MFA token');
}
// 3. セッションを生成
const sessionToken = await this.generateSessionToken(user.id);
return sessionToken;
}
}

SMS認証の実装:

// SMS認証の実装
class SMSAuth {
async sendVerificationCode(phoneNumber) {
// 6桁の認証コードを生成
const code = Math.floor(100000 + Math.random() * 900000).toString();
// 認証コードを保存(5分間有効)
await db.verificationCodes.create({
phoneNumber,
code,
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5分後
});
// SMSを送信
await smsService.send(phoneNumber, `認証コード: ${code}`);
return { sent: true };
}
async verifyCode(phoneNumber, code) {
// 認証コードを検証
const verificationCode = await db.verificationCodes.findOne({
phoneNumber,
code,
expiresAt: { $gt: new Date() }, // 有効期限内
used: false,
});
if (!verificationCode) {
throw new Error('Invalid or expired verification code');
}
// 認証コードを使用済みにする
await db.verificationCodes.update(verificationCode.id, {
used: true,
usedAt: new Date(),
});
return { verified: true };
}
}

OAuth 2.0認証の実装:

// OAuth 2.0認証の実装
const axios = require('axios');
class OAuth2Auth {
constructor() {
this.clientId = process.env.OAUTH_CLIENT_ID;
this.clientSecret = process.env.OAUTH_CLIENT_SECRET;
this.redirectUri = process.env.OAUTH_REDIRECT_URI;
this.authorizationUrl = 'https://oauth.provider.com/authorize';
this.tokenUrl = 'https://oauth.provider.com/token';
}
getAuthorizationUrl(state) {
// 認証URLを生成
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: 'openid profile email',
state, // CSRF対策
});
return `${this.authorizationUrl}?${params.toString()}`;
}
async exchangeCodeForToken(code) {
// 認証コードをトークンに交換
const response = await axios.post(this.tokenUrl, {
grant_type: 'authorization_code',
code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret,
});
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresIn: response.data.expires_in,
};
}
async getUserInfo(accessToken) {
// ユーザー情報を取得
const response = await axios.get('https://oauth.provider.com/userinfo', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return {
id: response.data.sub,
email: response.data.email,
name: response.data.name,
};
}
async authenticateWithOAuth(code, state) {
// 1. stateを検証(CSRF対策)
const storedState = await this.getStoredState();
if (state !== storedState) {
throw new Error('Invalid state');
}
// 2. 認証コードをトークンに交換
const tokens = await this.exchangeCodeForToken(code);
// 3. ユーザー情報を取得
const userInfo = await this.getUserInfo(tokens.accessToken);
// 4. ユーザーを作成または更新
let user = await db.users.findOne({ oauthId: userInfo.id });
if (!user) {
user = await db.users.create({
oauthId: userInfo.id,
email: userInfo.email,
name: userInfo.name,
oauthProvider: 'oauth',
});
}
// 5. セッションを生成
const sessionToken = await this.generateSessionToken(user.id);
return sessionToken;
}
}

JWT認証の実装:

// JWT認証の実装
const jwt = require('jsonwebtoken');
class JWTAuth {
constructor() {
this.secret = process.env.JWT_SECRET;
this.accessTokenExpiry = '15m'; // 15分
this.refreshTokenExpiry = '7d'; // 7日
}
generateAccessToken(userId) {
// アクセストークンを生成
return jwt.sign(
{ userId, type: 'access' },
this.secret,
{ expiresIn: this.accessTokenExpiry }
);
}
generateRefreshToken(userId) {
// リフレッシュトークンを生成
return jwt.sign(
{ userId, type: 'refresh' },
this.secret,
{ expiresIn: this.refreshTokenExpiry }
);
}
verifyToken(token) {
// トークンを検証
try {
const decoded = jwt.verify(token, this.secret);
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
throw new Error('Invalid token');
}
}
async authenticate(email, password) {
// パスワード認証
const user = await passwordAuth.authenticate(email, password);
// アクセストークンとリフレッシュトークンを生成
const accessToken = this.generateAccessToken(user.id);
const refreshToken = this.generateRefreshToken(user.id);
// リフレッシュトークンを保存
await db.refreshTokens.create({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7日後
});
return {
accessToken,
refreshToken,
};
}
async refreshAccessToken(refreshToken) {
// リフレッシュトークンを検証
const decoded = this.verifyToken(refreshToken);
// データベースでリフレッシュトークンを確認
const storedToken = await db.refreshTokens.findOne({
userId: decoded.userId,
token: refreshToken,
expiresAt: { $gt: new Date() },
revoked: false,
});
if (!storedToken) {
throw new Error('Invalid refresh token');
}
// 新しいアクセストークンを生成
const accessToken = this.generateAccessToken(decoded.userId);
return { accessToken };
}
}

安全なセッション管理:

// 安全なセッション管理
class SessionManager {
async createSession(userId) {
// セッションIDを生成(ランダムで推測不可能)
const sessionId = this.generateSecureSessionId();
// セッションを保存
await db.sessions.create({
sessionId,
userId,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24時間後
ipAddress: this.getClientIp(),
userAgent: this.getUserAgent(),
});
// CookieにセッションIDを設定
this.setCookie('sessionId', sessionId, {
httpOnly: true, // JavaScriptからアクセス不可
secure: true, // HTTPSのみ
sameSite: 'strict', // CSRF対策
maxAge: 24 * 60 * 60 * 1000, // 24時間
});
return sessionId;
}
async validateSession(sessionId) {
// セッションを検証
const session = await db.sessions.findOne({
sessionId,
expiresAt: { $gt: new Date() },
revoked: false,
});
if (!session) {
throw new Error('Invalid or expired session');
}
// セッションの更新(スライディングエクスピレーション)
await db.sessions.update(session.id, {
lastAccessedAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
});
return session;
}
async revokeSession(sessionId) {
// セッションを無効化
await db.sessions.update(
{ sessionId },
{ revoked: true, revokedAt: new Date() }
);
}
async revokeAllUserSessions(userId) {
// ユーザーのすべてのセッションを無効化
await db.sessions.update(
{ userId, revoked: false },
{ revoked: true, revokedAt: new Date() }
);
}
}

認証試行のレート制限:

// 認証試行のレート制限
class RateLimiter {
async checkRateLimit(identifier, action, maxAttempts, windowMs) {
const key = `rate_limit:${action}:${identifier}`;
// 現在の試行回数を取得
const attempts = await redis.incr(key);
// 初回の試行の場合、有効期限を設定
if (attempts === 1) {
await redis.expire(key, Math.ceil(windowMs / 1000));
}
// 試行回数が上限を超えた場合
if (attempts > maxAttempts) {
const ttl = await redis.ttl(key);
throw new Error(`Too many attempts. Please try again in ${ttl} seconds.`);
}
return { attempts, remaining: maxAttempts - attempts };
}
async limitLoginAttempts(email) {
// ログイン試行を5回/15分に制限
return await this.checkRateLimit(email, 'login', 5, 15 * 60 * 1000);
}
async limitPasswordReset(email) {
// パスワードリセットを3回/時間に制限
return await this.checkRateLimit(email, 'password_reset', 3, 60 * 60 * 1000);
}
}

認証方式のポイント:

  • パスワード認証: 強力なハッシュアルゴリズム、強度チェック、適切な管理
  • 多要素認証: TOTP、SMS認証など、複数の認証要素を組み合わせる
  • OAuth 2.0: サードパーティ認証、適切な実装とセキュリティ対策
  • JWT認証: アクセストークンとリフレッシュトークンの適切な管理
  • セッション管理: 安全なセッションID、適切な有効期限、セッションの無効化
  • レート制限: 認証試行の制限、ブルートフォース攻撃の防止

適切な認証方式を選択し、実装することで、セキュリティを確保できます。