Skip to main content

Firebase Authentication セキュリティベストプラクティス

🔐 認証セキュリティ基本原則

多層防御戦略

// セキュリティレベル定義
enum SecurityLevel {
BASIC = 1, // メール認証のみ
STANDARD = 2, // MFA対応
PREMIUM = 3 // 生体認証 + 高度な脅威検知
}

interface SecurityPolicy {
level: SecurityLevel
passwordPolicy: PasswordPolicy
sessionTimeout: number
maxFailedAttempts: number
accountLockoutDuration: number
requiredMFA: boolean
}

ブランド別セキュリティポリシー

const brandSecurityPolicies: Record<Brand, SecurityPolicy> = {
neko: {
level: SecurityLevel.PREMIUM,
passwordPolicy: {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
preventCommon: true
},
sessionTimeout: 7200, // 2時間
maxFailedAttempts: 3,
accountLockoutDuration: 1800, // 30分
requiredMFA: true
},

tokinoe: {
level: SecurityLevel.STANDARD,
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireNumbers: true,
preventCommon: true
},
sessionTimeout: 3600, // 1時間
maxFailedAttempts: 5,
accountLockoutDuration: 900, // 15分
requiredMFA: false
},

dog: {
level: SecurityLevel.BASIC,
passwordPolicy: {
minLength: 6,
requireNumbers: true
},
sessionTimeout: 1800, // 30分
maxFailedAttempts: 8,
accountLockoutDuration: 300, // 5分
requiredMFA: false
}
}

🔒 パスワードセキュリティ

強力なパスワードポリシー実装

// パスワード強度チェッカー
class PasswordValidator {
private readonly commonPasswords = new Set([
'password', '123456', 'password123', 'admin', 'qwerty'
// ... その他よく使われるパスワード
])

validate(password: string, brand: Brand): ValidationResult {
const policy = brandSecurityPolicies[brand].passwordPolicy
const errors: string[] = []

// 長さチェック
if (password.length < policy.minLength) {
errors.push(`パスワードは${policy.minLength}文字以上である必要があります`)
}

// 複雑性チェック
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('大文字を含む必要があります')
}

if (policy.requireLowercase && !/[a-z]/.test(password)) {
errors.push('小文字を含む必要があります')
}

if (policy.requireNumbers && !/\d/.test(password)) {
errors.push('数字を含む必要があります')
}

if (policy.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('特殊文字を含む必要があります')
}

// よく使われるパスワードチェック
if (policy.preventCommon && this.commonPasswords.has(password.toLowerCase())) {
errors.push('一般的すぎるパスワードは使用できません')
}

return {
isValid: errors.length === 0,
errors,
strength: this.calculateStrength(password)
}
}

private calculateStrength(password: string): 'weak' | 'medium' | 'strong' {
let score = 0

if (password.length >= 8) score += 1
if (password.length >= 12) score += 1
if (/[a-z]/.test(password)) score += 1
if (/[A-Z]/.test(password)) score += 1
if (/\d/.test(password)) score += 1
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1

if (score <= 2) return 'weak'
if (score <= 4) return 'medium'
return 'strong'
}
}

🔐 多要素認証 (MFA)

TOTP実装

import { AuthenticatorSecret } from 'firebase/auth'

class MFAManager {

// TOTP設定
async setupTOTP(user: User): Promise<TOTPSetupResult> {
try {
const multiFactorSession = await multiFactor(user).getSession()
const totpGenerator = new TotpMultiFactorGenerator()

const totpSecret = await totpGenerator.generateSecret(multiFactorSession)

// QRコード用URL生成
const qrCodeUrl = this.generateTOTPUrl(totpSecret, user.email!)

return {
secret: totpSecret,
qrCodeUrl,
backupCodes: this.generateBackupCodes()
}
} catch (error) {
throw new Error(`TOTP設定エラー: ${error.message}`)
}
}

// TOTP認証
async verifyTOTP(user: User, code: string): Promise<boolean> {
try {
const multiFactorSession = await multiFactor(user).getSession()
const totpAssertion = TotpMultiFactorGenerator.assertionForSignIn(
user.multiFactor.enrolledFactors[0].uid,
code
)

await multiFactor(user).enroll(totpAssertion, multiFactorSession)
return true
} catch (error) {
console.error('TOTP認証エラー:', error)
return false
}
}

private generateTOTPUrl(secret: string, email: string): string {
const issuer = 'Nekomata Print'
return `otpauth://totp/${issuer}:${email}?secret=${secret}&issuer=${issuer}`
}

private generateBackupCodes(): string[] {
const codes = []
for (let i = 0; i < 10; i++) {
codes.push(Math.random().toString(36).substr(2, 8).toUpperCase())
}
return codes
}
}

SMS認証

// SMS MFA実装
class SMSAuthenticator {
async sendVerificationCode(phoneNumber: string, brand: Brand): Promise<void> {
try {
// Twilioまたは他のSMSプロバイダーを使用
const code = this.generateVerificationCode()

await this.smsProvider.send({
to: phoneNumber,
message: `${getBrandConfig(brand).displayName} 認証コード: ${code}`,
validityPeriod: 300 // 5分間有効
})

// Redisまたは一時ストレージに保存
await this.storeVerificationCode(phoneNumber, code, 300)

} catch (error) {
throw new Error(`SMS送信エラー: ${error.message}`)
}
}

async verifyCode(phoneNumber: string, code: string): Promise<boolean> {
try {
const storedCode = await this.getStoredCode(phoneNumber)

if (!storedCode || storedCode !== code) {
return false
}

// 使用済みコードを削除
await this.deleteVerificationCode(phoneNumber)
return true

} catch (error) {
console.error('SMS認証エラー:', error)
return false
}
}

private generateVerificationCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString()
}
}

🛡️ セッション管理

安全なセッション実装

class SecureSessionManager {
private readonly sessionStore = new Map<string, SessionData>()

async createSession(user: AuthUser, brand: Brand): Promise<SessionToken> {
const policy = brandSecurityPolicies[brand]
const sessionId = this.generateSecureId()

const sessionData: SessionData = {
userId: user.uid,
brand,
createdAt: new Date(),
expiresAt: new Date(Date.now() + policy.sessionTimeout * 1000),
ipAddress: this.getUserIP(),
userAgent: this.getUserAgent(),
isActive: true,
lastActivity: new Date()
}

// セッションデータを暗号化して保存
const encryptedSession = await this.encryptSessionData(sessionData)
await this.storeSession(sessionId, encryptedSession)

// JWTトークン生成
const token = jwt.sign(
{
sessionId,
userId: user.uid,
brand,
exp: Math.floor(sessionData.expiresAt.getTime() / 1000)
},
process.env.JWT_SECRET!,
{ algorithm: 'HS256' }
)

return { token, expiresAt: sessionData.expiresAt }
}

async validateSession(token: string): Promise<SessionValidationResult> {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
const sessionData = await this.getSessionData(decoded.sessionId)

if (!sessionData || !sessionData.isActive) {
return { isValid: false, reason: 'Session not found or inactive' }
}

if (sessionData.expiresAt < new Date()) {
await this.invalidateSession(decoded.sessionId)
return { isValid: false, reason: 'Session expired' }
}

// セッションハイジャック検知
if (!this.validateSessionFingerprint(sessionData)) {
await this.invalidateSession(decoded.sessionId)
return { isValid: false, reason: 'Session fingerprint mismatch' }
}

// アクティビティ更新
await this.updateSessionActivity(decoded.sessionId)

return {
isValid: true,
sessionData: {
userId: sessionData.userId,
brand: sessionData.brand,
lastActivity: sessionData.lastActivity
}
}
} catch (error) {
return { isValid: false, reason: 'Invalid token' }
}
}

private validateSessionFingerprint(session: SessionData): boolean {
const currentIP = this.getUserIP()
const currentUA = this.getUserAgent()

// IPアドレス変更の検知(プロキシ考慮)
if (session.ipAddress !== currentIP) {
// 地理的距離による異常検知
if (this.isGeographicallyAnomalous(session.ipAddress, currentIP)) {
return false
}
}

// User-Agent変更の検知
if (session.userAgent !== currentUA) {
return false
}

return true
}
}

🚨 脅威検知と対策

異常ログイン検知

class ThreatDetectionSystem {

async analyzeLoginAttempt(attempt: LoginAttempt): Promise<ThreatLevel> {
const riskFactors = []

// 地理的異常検知
if (await this.isAnomalousLocation(attempt)) {
riskFactors.push('unusual_location')
}

// 時間帯異常検知
if (this.isAnomalousTime(attempt)) {
riskFactors.push('unusual_time')
}

// デバイス異常検知
if (await this.isUnknownDevice(attempt)) {
riskFactors.push('unknown_device')
}

// 短時間での複数ログイン試行
if (await this.isBruteForceAttempt(attempt)) {
riskFactors.push('brute_force')
}

// 脅威レベル計算
return this.calculateThreatLevel(riskFactors)
}

private async isAnomalousLocation(attempt: LoginAttempt): Promise<boolean> {
const userHistory = await this.getUserLoginHistory(attempt.userId, 30) // 30日間
const locations = userHistory.map(h => h.location)

// 過去のログイン地点から大きく離れているかチェック
const isAnomalous = locations.every(loc =>
this.calculateDistance(loc, attempt.location) > 500 // 500km以上
)

return isAnomalous && locations.length > 5
}

async handleThreatDetection(attempt: LoginAttempt, threatLevel: ThreatLevel): Promise<void> {
switch (threatLevel) {
case ThreatLevel.LOW:
// ログのみ記録
await this.logSecurityEvent('low_risk_login', attempt)
break

case ThreatLevel.MEDIUM:
// 追加認証要求
await this.requireAdditionalVerification(attempt)
await this.notifyUser('suspicious_activity', attempt)
break

case ThreatLevel.HIGH:
// アカウント一時ロック
await this.temporaryAccountLock(attempt.userId)
await this.notifyUser('account_locked', attempt)
await this.notifyAdmins('high_risk_detection', attempt)
break

case ThreatLevel.CRITICAL:
// 即座にアカウント無効化
await this.disableAccount(attempt.userId)
await this.notifyUser('account_disabled', attempt)
await this.escalateToSecurity('critical_threat', attempt)
break
}
}
}

🔐 データ暗号化

PII データ保護

class PIIProtection {
private readonly encryptionKey = process.env.ENCRYPTION_KEY!

// 個人情報の暗号化
async encryptPII(data: PIIData): Promise<EncryptedData> {
const cipher = crypto.createCipher('aes-256-gcm', this.encryptionKey)

let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex')
encrypted += cipher.final('hex')

const authTag = cipher.getAuthTag()

return {
encryptedData: encrypted,
authTag: authTag.toString('hex'),
algorithm: 'aes-256-gcm'
}
}

// 個人情報の復号化
async decryptPII(encryptedData: EncryptedData): Promise<PIIData> {
const decipher = crypto.createDecipher('aes-256-gcm', this.encryptionKey)
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'))

let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8')
decrypted += decipher.final('utf8')

return JSON.parse(decrypted)
}

// データマスキング(ログ用)
maskSensitiveData(data: any): any {
const masked = { ...data }

// メールアドレス
if (masked.email) {
masked.email = this.maskEmail(masked.email)
}

// 電話番号
if (masked.phone) {
masked.phone = this.maskPhone(masked.phone)
}

// 住所
if (masked.address) {
masked.address = this.maskAddress(masked.address)
}

return masked
}

private maskEmail(email: string): string {
const [local, domain] = email.split('@')
const maskedLocal = local[0] + '*'.repeat(local.length - 2) + local[local.length - 1]
return `${maskedLocal}@${domain}`
}
}

📊 セキュリティ監査

監査ログシステム

class SecurityAuditLogger {

async logSecurityEvent(event: SecurityEvent): Promise<void> {
const auditEntry: AuditLogEntry = {
timestamp: new Date().toISOString(),
eventType: event.type,
userId: event.userId,
brand: event.brand,
ipAddress: this.hashIP(event.ipAddress), // プライバシー考慮
userAgent: event.userAgent,
result: event.result,
riskLevel: event.riskLevel,
additionalData: this.sanitizeData(event.additionalData)
}

// 複数の保存先に記録(冗長化)
await Promise.all([
this.writeToDatabase(auditEntry),
this.writeToLogFile(auditEntry),
this.sendToSIEM(auditEntry) // Security Information and Event Management
])
}

async generateSecurityReport(brand: Brand, timeframe: TimeFrame): Promise<SecurityReport> {
const events = await this.getAuditEvents(brand, timeframe)

return {
brand,
timeframe,
totalAttempts: events.length,
successfulLogins: events.filter(e => e.result === 'success').length,
failedAttempts: events.filter(e => e.result === 'failure').length,
blockedAttempts: events.filter(e => e.result === 'blocked').length,
threatLevelDistribution: this.calculateThreatDistribution(events),
recommendedActions: await this.generateRecommendations(events)
}
}

private hashIP(ip: string): string {
// IPアドレスをハッシュ化(GDPR対応)
return crypto.createHash('sha256').update(ip + process.env.IP_SALT).digest('hex').substr(0, 16)
}
}

🔄 インシデント対応

セキュリティインシデント対応フロー

class IncidentResponseSystem {

async handleSecurityIncident(incident: SecurityIncident): Promise<void> {
// 1. 初期評価
const severity = await this.assessIncidentSeverity(incident)

// 2. 関係者への通知
await this.notifyStakeholders(incident, severity)

// 3. 即座の対策実行
await this.executeImmediateResponse(incident, severity)

// 4. 詳細調査開始
const investigation = await this.startInvestigation(incident)

// 5. 継続監視
await this.setupContinuousMonitoring(incident)

// 6. インシデントログ記録
await this.logIncident(incident, severity, investigation)
}

private async executeImmediateResponse(
incident: SecurityIncident,
severity: IncidentSeverity
): Promise<void> {

switch (severity) {
case IncidentSeverity.CRITICAL:
// 全システム一時停止
await this.emergencySystemShutdown()
break

case IncidentSeverity.HIGH:
// 影響のあるアカウント無効化
await this.disableAffectedAccounts(incident.affectedUsers)
// MFA強制有効化
await this.forceMFAForAllUsers()
break

case IncidentSeverity.MEDIUM:
// 関連セッション無効化
await this.invalidateRelatedSessions(incident.indicators)
// 監視強化
await this.enhanceMonitoring(incident.type)
break

case IncidentSeverity.LOW:
// ログ詳細化
await this.enhanceLogging(incident.type)
break
}
}
}

最終更新: 2025-08-23 18:10:58 JST
セキュリティレベル: Enterprise Grade
コンプライアンス: GDPR, CCPA, 個人情報保護法 対応済み