diff --git a/CHANGELOG.md b/CHANGELOG.md index d084006562c..efcabfdcaf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ - Enhance: リモートノートクリーニングジョブの削除対象検索処理のパフォーマンス改善 - Fix: センシティブメディア自動検出周りの依存関係・ファイルの解決に失敗する問題を修正 - Fix: フォロワー限定投稿を指名投稿で引用した際に、引用した投稿の公開範囲が意図せず変更される問題を修正 +- Fix: 誤ったワンタイムパスワードを入力した際に500エラーが返される問題を修正 +- Enhance: 認証系エンドポイントのパスワード・TOTP検証エラーをINVALID_CREDENTIALに統合 ## 2026.5.4 diff --git a/packages/backend/src/core/UserAuthService.ts b/packages/backend/src/core/UserAuthService.ts index bdc27cbe8e5..5a12c57c583 100644 --- a/packages/backend/src/core/UserAuthService.ts +++ b/packages/backend/src/core/UserAuthService.ts @@ -14,6 +14,8 @@ import type { MiLocalUser } from '@/models/User.js'; @Injectable() export class UserAuthService { + public static AuthenticationFailedError = class extends Error {}; + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -38,7 +40,7 @@ export class UserAuthService { }); if (delta === null) { - throw new Error('authentication failed'); + throw new UserAuthService.AuthenticationFailedError(); } } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 2a30e8b0c3e..a4b9d1e0874 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -10,12 +10,27 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { requireCredential: true, secure: true, + errors: { + notInitiated: { + message: '2fa setup has not been initiated.', + code: '2FA_SETUP_NOT_INITIATED', + id: '283f18c1-5b84-4699-a7a4-2beec808b74c', + }, + + verificationFailed: { + message: 'Verification failed. Please try again.', + code: 'VERIFICATION_FAILED', + id: '90a0971b-f73a-4993-b224-8307ba7421e7', + }, + }, + res: { type: 'object', properties: { @@ -53,7 +68,7 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); if (profile.twoFactorTempSecret == null) { - throw new Error('二段階認証の設定が開始されていません'); + throw new ApiError(meta.errors.notInitiated); } const delta = OTPAuth.TOTP.validate({ @@ -64,7 +79,7 @@ export default class extends Endpoint { // eslint- }); if (delta === null) { - throw new Error('not verified'); + throw new ApiError(meta.errors.verificationFailed); } const backupCodes = Array.from({ length: 5 }, () => new OTPAuth.Secret().base32); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 8dc5cafb560..d9f4c7b0193 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -26,6 +26,12 @@ export const meta = { id: '0d7ec6d2-e652-443e-a7bf-9ee9a0cd77b0', }, + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: '430da61f-f346-411b-8aa0-fdeb0736dbe4', + }, + twoFactorNotEnabled: { message: '2fa not enabled.', code: 'TWO_FACTOR_NOT_ENABLED', @@ -76,18 +82,24 @@ export default class extends Endpoint { if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 9d1d595c900..98655a10bd3 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -30,6 +30,12 @@ export const meta = { id: '38769596-efe2-4faf-9bec-abbb3f2cd9ba', }, + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: 'a1189f08-e4a2-462e-b5de-647c312efadb', + }, + twoFactorNotEnabled: { message: '2fa not enabled.', code: 'TWO_FACTOR_NOT_ENABLED', @@ -76,18 +82,24 @@ export default class extends Endpoint { if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index b6c837eda7d..b95ff4f4e29 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -25,6 +25,12 @@ export const meta = { code: 'INCORRECT_PASSWORD', id: '78d6c839-20c9-4c66-b90a-fc0542168b48', }, + + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: '4ccb213e-227e-4c18-8ddb-f2dddb31a9d0', + }, }, res: { @@ -67,18 +73,24 @@ export default class extends Endpoint { // eslint- if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 6e5d9943def..8ec99f21491 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -24,6 +24,12 @@ export const meta = { code: 'INCORRECT_PASSWORD', id: '141c598d-a825-44c8-9173-cfb9d92be493', }, + + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: 'c1c5fa72-f66a-4935-b36a-cb8259e4b03e', + }, }, } as const; @@ -56,18 +62,24 @@ export default class extends Endpoint { // eslint- if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 23b577dc18b..b757d742939 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -24,6 +24,12 @@ export const meta = { code: 'INCORRECT_PASSWORD', id: '7add0395-9901-4098-82f9-4f67af65f775', }, + + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: '5ce7354e-d1d0-4305-b37c-9825b766f03f', + }, }, } as const; @@ -52,18 +58,24 @@ export default class extends Endpoint { // eslint- if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index 19ea187ee86..6ff4878fbde 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -8,12 +8,27 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: 'd46ffe5c-200b-4471-aca2-4d0ef197368f', + }, + + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: '1e5d4005-3eb0-43f8-b466-87ad864b9fd6', + }, + }, } as const; export const paramDef = { @@ -40,20 +55,26 @@ export default class extends Endpoint { // eslint- if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.currentPassword, profile.password!); if (!passwordMatched) { - throw new Error('incorrect password'); + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } + throw new ApiError(meta.errors.incorrectPassword); } // Generate hash of password diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 42324c7778a..92d89d360b6 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -9,12 +9,27 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: '9d72604c-9d55-4511-9b96-de11900925c7', + }, + + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: 'd5af1163-2248-404f-a3d9-7b8c9e019723', + }, + }, } as const; export const paramDef = { @@ -44,13 +59,16 @@ export default class extends Endpoint { // eslint- if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } @@ -61,7 +79,10 @@ export default class extends Endpoint { // eslint- const passwordMatched = await bcrypt.compare(ps.password, profile.password!); if (!passwordMatched) { - throw new Error('incorrect password'); + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } + throw new ApiError(meta.errors.incorrectPassword); } await this.deleteAccountService.deleteAccount(me); diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index c2f4281f369..8f244f1faa2 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -34,6 +34,12 @@ export const meta = { id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3', }, + invalidCredential: { + message: 'Invalid credential.', + code: 'INVALID_CREDENTIAL', + id: 'b8231819-bd85-4c24-83f1-657a3e0efad8', + }, + unavailable: { message: 'Unavailable email address.', code: 'UNAVAILABLE', @@ -86,18 +92,24 @@ export default class extends Endpoint { // eslint- if (profile.twoFactorEnabled) { if (token == null) { - throw new Error('authentication failed'); + throw new ApiError(meta.errors.invalidCredential); } try { await this.userAuthService.twoFactorAuthenticate(profile, token); - } catch (_) { - throw new Error('authentication failed'); + } catch (e) { + if (e instanceof UserAuthService.AuthenticationFailedError) { + throw new ApiError(meta.errors.invalidCredential); + } + throw e; } } const passwordMatched = await bcrypt.compare(ps.password, profile.password!); if (!passwordMatched) { + if (profile.twoFactorEnabled) { + throw new ApiError(meta.errors.invalidCredential); + } throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index a3b44134369..e50a45992e5 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -132,15 +132,10 @@ async function unregisterTOTP(): Promise { os.apiWithDialog('i/2fa/unregister', { password: auth.result.password, token: auth.result.token, - }).then(res => { + }).then(() => { updateCurrentAccountPartial({ twoFactorEnabled: false, }); - }).catch(error => { - os.alert({ - type: 'error', - text: error, - }); }); }