diff --git a/packages/backend/src/core/UserAuthService.ts b/packages/backend/src/core/UserAuthService.ts index bdc27cbe8e..dc9104e8a2 100644 --- a/packages/backend/src/core/UserAuthService.ts +++ b/packages/backend/src/core/UserAuthService.ts @@ -4,17 +4,18 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { QueryFailedError } from 'typeorm'; +import * as Redis from 'ioredis'; import * as OTPAuth from 'otpauth'; import { DI } from '@/di-symbols.js'; import type { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { MiLocalUser } from '@/models/User.js'; @Injectable() export class UserAuthService { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -30,16 +31,43 @@ export class UserAuthService { twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token), }); } else { - const delta = OTPAuth.TOTP.validate({ + // 1. 判定に用いるタイムスタンプを固定 + const now = Date.now(); + const normalizedToken = token.trim(); + const validationWindow = 1; + const timeStep = 30; // TOTPの周期(秒) + + // 2. TOTPインスタンスを生成(設定を一元管理するため) + const totp = new OTPAuth.TOTP({ secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!), digits: 6, - token, - window: 5, + period: timeStep, + }); + + // 3. 固定したタイムスタンプを使って検証 + const delta = totp.validate({ + token: normalizedToken, + window: validationWindow, + timestamp: now, }); if (delta === null) { throw new Error('authentication failed'); } + + // 4. totp.counter() を用い、同じタイムスタンプから基準ステップを取得 + const currentStep = totp.counter({ timestamp: now }); + const step = currentStep + delta; + + const usedTokenRedisKey = `2fa:used:${profile.userId}:${step}`; + + // 5. TTL(有効期限)の設定 + const ttl = timeStep * (validationWindow * 2 + 1); + const setResult = await this.redisClient.set(usedTokenRedisKey, normalizedToken, 'EX', ttl, 'NX'); + + if (setResult === null) { + throw new Error('authentication failed'); + } } } }