From 975c2e7bc567618c3f8b0082afcba6530d679dae Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:23:33 +0900 Subject: enhance(frontend): サインイン画面の改善 (#14658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update MkSignin.vue * Update MkSignin.vue * wip * Update CHANGELOG.md * enhance(frontend): サインイン画面の改善 * Update Changelog * 14655の変更取り込み * spdx * fix * fix * fix * :art: * :art: * :art: * :art: * Captchaがリセットされない問題を修正 * 次の処理をsignin apiから読み取るように * Add Comments * fix * fix test * attempt to fix test * fix test * fix test * fix test * fix * fix test * fix: 一部のエラーがちゃんと出るように * Update Changelog * :art: * :art: * remove border --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- .../backend/src/server/api/SigninApiService.ts | 86 +++++++++++++++++++--- 1 file changed, 74 insertions(+), 12 deletions(-) (limited to 'packages/backend/src/server/api/SigninApiService.ts') diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 2ccc75da00..81684beb3c 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -12,6 +12,7 @@ import type { MiMeta, SigninsRepository, UserProfilesRepository, + UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -25,9 +26,27 @@ import { CaptchaService } from '@/core/CaptchaService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; -import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'; import type { FastifyReply, FastifyRequest } from 'fastify'; +/** + * next を指定すると、次にクライアント側で行うべき処理を指定できる。 + * + * - `captcha`: パスワードと、(有効になっている場合は)CAPTCHAを求める + * - `password`: パスワードを求める + * - `totp`: ワンタイムパスワードを求める + * - `passkey`: WebAuthn認証を求める(WebAuthnに対応していないブラウザの場合はワンタイムパスワード) + */ + +type SigninErrorResponse = { + id: string; + next?: 'captcha' | 'password' | 'totp'; +} | { + id: string; + next: 'passkey'; + authRequest: PublicKeyCredentialRequestOptionsJSON; +}; + @Injectable() export class SigninApiService { constructor( @@ -43,6 +62,9 @@ export class SigninApiService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -60,7 +82,7 @@ export class SigninApiService { request: FastifyRequest<{ Body: { username: string; - password: string; + password?: string; token?: string; credential?: AuthenticationResponseJSON; 'hcaptcha-response'?: string; @@ -79,7 +101,7 @@ export class SigninApiService { const password = body['password']; const token = body['token']; - function error(status: number, error: { id: string }) { + function error(status: number, error: SigninErrorResponse) { reply.code(status); return { error }; } @@ -103,11 +125,6 @@ export class SigninApiService { return; } - if (typeof password !== 'string') { - reply.code(400); - return; - } - if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -132,11 +149,36 @@ export class SigninApiService { } const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1); + + if (password == null) { + reply.code(403); + if (profile.twoFactorEnabled) { + return { + error: { + id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf', + next: 'password', + }, + } satisfies { error: SigninErrorResponse }; + } else { + return { + error: { + id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf', + next: 'captcha', + }, + } satisfies { error: SigninErrorResponse }; + } + } + + if (typeof password !== 'string') { + reply.code(400); + return; + } // Compare password const same = await bcrypt.compare(password, profile.password!); - const fail = async (status?: number, failure?: { id: string }) => { + const fail = async (status?: number, failure?: SigninErrorResponse) => { // Append signin history await this.signinsRepository.insert({ id: this.idService.gen(), @@ -217,7 +259,7 @@ export class SigninApiService { id: '93b86c4b-72f9-40eb-9815-798928603d1e', }); } - } else { + } else if (securityKeysAvailable) { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', @@ -226,8 +268,28 @@ export class SigninApiService { const authRequest = await this.webAuthnService.initiateAuthentication(user.id); - reply.code(200); - return authRequest; + reply.code(403); + return { + error: { + id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4', + next: 'passkey', + authRequest, + }, + } satisfies { error: SigninErrorResponse }; + } else { + if (!same || !profile.twoFactorEnabled) { + return await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } else { + reply.code(403); + return { + error: { + id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf', + next: 'totp', + }, + } satisfies { error: SigninErrorResponse }; + } } // never get here } -- cgit v1.2.3-freya