From 5f42c200b98760092bc2fc02eee334e5692d57ed Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 12 Sep 2024 06:53:16 +0000 Subject: [PATCH 01/14] Sign in with passkey (PoC) --- packages/backend/src/core/WebAuthnService.ts | 72 +++++++++ packages/backend/src/server/ServerModule.ts | 2 + .../src/server/api/ApiServerService.ts | 9 ++ .../server/api/SigninWithPasskeyApiService.ts | 151 ++++++++++++++++++ packages/frontend/src/components/MkSignin.vue | 57 +++++++ packages/misskey-js/etc/misskey-js.api.md | 18 +++ packages/misskey-js/src/api.types.ts | 6 + packages/misskey-js/src/entities.ts | 10 ++ 8 files changed, 325 insertions(+) create mode 100644 packages/backend/src/server/api/SigninWithPasskeyApiService.ts diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index a40c6ff1c95e..acb170327f7f 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -164,6 +164,78 @@ export class WebAuthnService { return authenticationOptions; } + @bindThis + public async initiateSignInWithPasskeyAuthentication(context: string): Promise { + const relyingParty = await this.getRelyingParty(); + + const authenticationOptions = await generateAuthenticationOptions({ + rpID: relyingParty.rpId, + userVerification: 'preferred', + }); + + await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge); + + return authenticationOptions; + } + + @bindThis + public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise { + const challenge = await this.redisClient.get(`webauthn:challenge:${context}`); + + if (!challenge) { + throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found'); + } + + await this.redisClient.del(`webauthn:challenge:${context}`); + + const key = await this.userSecurityKeysRepository.findOneBy({ + id: response.id, + }); + + if (!key) { + throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'unknown key'); + } + + const relyingParty = await this.getRelyingParty(); + + let verification; + try { + verification = await verifyAuthenticationResponse({ + response: response, + expectedChallenge: challenge, + expectedOrigin: relyingParty.origin, + expectedRPID: relyingParty.rpId, + authenticator: { + credentialID: key.id, + credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), + counter: key.counter, + transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, + }, + requireUserVerification: true, + }); + } catch (error) { + console.error(error); + throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed'); + } + + const { verified, authenticationInfo } = verification; + + if (!verified) { + return null; + } + + await this.userSecurityKeysRepository.update({ + id: response.id, + }, { + lastUsed: new Date(), + counter: authenticationInfo.newCounter, + credentialDeviceType: authenticationInfo.credentialDeviceType, + credentialBackedUp: authenticationInfo.credentialBackedUp, + }); + + return key.userId; + } + @bindThis public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise { const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`); diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 12d50619856c..3ab0b815f232 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -46,6 +46,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; +import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @Module({ imports: [ @@ -71,6 +72,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js AuthenticateService, RateLimiterService, SigninApiService, + SigninWithPasskeyApiService, SigninService, SignupApiService, StreamingApiServerService, diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 13cbdfc3beb0..709a0446018a 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -8,6 +8,7 @@ import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; import { ModuleRef } from '@nestjs/core'; +import { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { Config } from '@/config.js'; import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -17,6 +18,7 @@ import endpoints from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; +import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @Injectable() @@ -37,6 +39,7 @@ export class ApiServerService { private apiCallService: ApiCallService, private signupApiService: SignupApiService, private signinApiService: SigninApiService, + private signinWithPasskeyApiService: SigninWithPasskeyApiService, ) { //this.createServer = this.createServer.bind(this); } @@ -131,6 +134,12 @@ export class ApiServerService { }; }>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); + fastify.post<{ + Body: { + credential?: AuthenticationResponseJSON; + }; + }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply)); + fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); fastify.get('/v1/instance/peers', async (request, reply) => { diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts new file mode 100644 index 000000000000..9ae52af8f488 --- /dev/null +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { + SigninsRepository, + UserProfilesRepository, + UsersRepository, +} from '@/models/_.js'; +import type { Config } from '@/config.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import { IdService } from '@/core/IdService.js'; +import { bindThis } from '@/decorators.js'; +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import Logger from '@/logger.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type { IdentifiableError } from '@/misc/identifiable-error.js'; +import { RateLimiterService } from './RateLimiterService.js'; +import { SigninService } from './SigninService.js'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import type { FastifyReply, FastifyRequest } from 'fastify'; + +@Injectable() +export class SigninWithPasskeyApiService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private idService: IdService, + private rateLimiterService: RateLimiterService, + private signinService: SigninService, + private webAuthnService: WebAuthnService, + ) { + } + + @bindThis + public async signin( + request: FastifyRequest<{ + Body: { + credential?: AuthenticationResponseJSON; + context?: string; + }; + }>, + reply: FastifyReply, + ) { + reply.header('Access-Control-Allow-Origin', this.config.url); + reply.header('Access-Control-Allow-Credentials', 'true'); + + const body = request.body; + const credential = body['credential']; + + function error(status: number, error: { id: string }) { + reply.code(status); + return { error }; + } + + const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => { + // Append signin history + await this.signinsRepository.insert({ + id: this.idService.gen(), + userId: userId, + ip: request.ip, + headers: request.headers as any, + success: false, + }); + return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); + }; + + try { + // not more than 1 attempt per second and not more than 500 attempts per hour + await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 500, minInterval: 1000 }, getIpHash(request.ip)); + } catch (err) { + reply.code(429); + return { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + } + + // Initiate Passkey Auth with context + if (!credential) { + const context = randomUUID(); + const authRequest = { + option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context), + context: context, + }; + reply.code(200); + return authRequest; + } + + const context = body.context; + console.log(`passkey auth context: ${context}`); + if (!context || typeof context !== 'string') { + reply.code(400); + return; + } + const authorizedUserId: MiUser['id'] | null = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); + + if (authorizedUserId == null) { + return error(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } + + // Fetch user + const user = await this.usersRepository.findOneBy({ + id: authorizedUserId, + host: IsNull(), + }) as MiLocalUser | null; + + if (user == null) { + return error(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } + + if (user.isSuspended) { + return error(403, { + id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', + }); + } + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + if (!profile.usePasswordLessLogin) { + return await fail(user.id, 403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } + + return this.signinService.signin(request, reply, user); + } +} diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 231a6dfcf5c5..4b0d4b6d40d8 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -57,6 +57,12 @@ SPDX-License-Identifier: AGPL-3.0-only {{ signing ? i18n.ts.loggingIn : i18n.ts.login }} +
+

{{ i18n.ts.useSecurityKey }}

+ + {{ i18n.ts.login }} + +
@@ -66,6 +72,7 @@ import { defineAsyncComponent, ref } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; +import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; import { query, extractDomain } from '@@/js/url.js'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; @@ -150,6 +157,56 @@ async function queryKey(): Promise { }); } +const passkey_context = ref(''); + +function onPasskey(): void { + signing.value = true; + if (webAuthnSupported()) { + misskeyApi('signin-with-passkey', {}) + .then((res) => { + totpLogin.value = false; + signing.value = false; + queryingKey.value = true; + passkey_context.value = res.context; + credentialRequest = parseRequestOptionsFromJSON({ + publicKey: res.option, + }); + }) + .then(() => queryPasskey()) + .catch(loginFailed); + } +} + +async function queryPasskey(): Promise { + if (credentialRequest == null) return; + queryingKey.value = true; + console.log('Waiting passkey auth...'); + await webAuthnRequest(credentialRequest) + .catch((er) => { + console.warn('Fail!!', er); + queryingKey.value = false; + return Promise.reject(null); + }).then(credential => { + credentialRequest = null; + queryingKey.value = false; + signing.value = true; + return misskeyApi('signin-with-passkey', { + credential: credential.toJSON(), + context: passkey_context.value, + }); + }).then(res => { + emit('login', res); + return onLogin(res); + }).catch(err => { + if (err === null) return; + os.alert({ + type: 'error', + text: i18n.ts.signinFailed, + }); + signing.value = false; + }); +} + function onSubmit(): void { signing.value = true; if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 9ffd0aa02562..30cbca5e434f 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1160,6 +1160,10 @@ export type Endpoints = Overwrite; res: AdminRolesCreateResponse; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 08d3dc5c6d37..a648aafdea71 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -271,6 +271,16 @@ export type SigninRequest = { token?: string; }; +export type SigninWithPasskeyRequest = { + credential?: object; + context?: string; +}; + +export type SigninWithPasskeyResponse = { + option?: object; + context?: string; +} | SigninResponse; + export type SigninResponse = { id: User['id'], i: string, From b5cfc018e72caf84c1c6475c6697a16e6ec83570 Mon Sep 17 00:00:00 2001 From: Squarecat-meow Date: Thu, 12 Sep 2024 14:10:29 +0000 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=92=84=20Added=20"Login=20with=20Pa?= =?UTF-8?q?sskey"=20Button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en-US.yml | 2 ++ locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + locales/ko-KR.yml | 2 ++ packages/frontend/src/components/MkSignin.vue | 11 +++++++---- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index c82ea3c9a27c..6ffc4d3d1c12 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1263,6 +1263,8 @@ confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?" createdLists: "Created lists" createdAntennas: "Created antennas" +signinWithPasskey: "Login With Passkey" + _delivery: status: "Delivery status" stop: "Suspended" diff --git a/locales/index.d.ts b/locales/index.d.ts index 2a27eb3e15fa..aff027811fbd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5108,6 +5108,10 @@ export interface Locale extends ILocale { * {n}件の変更があります */ "thereAreNChanges": ParameterizedString<"n">; + /** + * パスキーでログイン + */ + "signinWithPasskey": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 80cd8dc7cc32..2af3d62392a0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1273,6 +1273,7 @@ performance: "パフォーマンス" modified: "変更あり" discard: "破棄" thereAreNChanges: "{n}件の変更があります" +signinWithPasskey: "パスキーでログイン" _delivery: status: "配信状態" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 34c1cc3ebfb5..ca812113f398 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1249,6 +1249,8 @@ alwaysConfirmFollow: "팔로우일 때 항상 확인하기" inquiry: "문의하기" tryAgain: "다시 시도해 주세요." confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인" +signinWithPasskey: "패스키로 로그인" + _delivery: status: "전송 상태" stop: "정지됨" diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 4b0d4b6d40d8..34d09ae85ccb 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -58,10 +58,12 @@ SPDX-License-Identifier: AGPL-3.0-only
-

{{ i18n.ts.useSecurityKey }}

- - {{ i18n.ts.login }} + + + + {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} +

{{ i18n.ts.useSecurityKey }}

@@ -84,6 +86,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; +import MkDivider from './MkDivider.vue'; const signing = ref(false); const user = ref(null); @@ -194,7 +197,7 @@ async function queryPasskey(): Promise { credential: credential.toJSON(), context: passkey_context.value, }); - }).then(res => { + }).then((res: SigninWithPasskeyResponse) => { emit('login', res); return onLogin(res); }).catch(err => { From e11b8176c06455acb494bb57f99c2ef16ba1143c Mon Sep 17 00:00:00 2001 From: Yuno Date: Thu, 12 Sep 2024 14:24:06 +0000 Subject: [PATCH 03/14] refactor: Improve error response when WebAuthn challenge fails --- packages/backend/src/core/WebAuthnService.ts | 5 ++++ .../server/api/SigninWithPasskeyApiService.ts | 23 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index acb170327f7f..c22769aa14fd 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -178,6 +178,11 @@ export class WebAuthnService { return authenticationOptions; } + /** + * Verify Webauthn AuthenticationCredential + * @throws IdentifiableError + * @returns MiUser['id'] or null + */ @bindThis public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise { const challenge = await this.redisClient.get(`webauthn:challenge:${context}`); diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 9ae52af8f488..6f21c45d595a 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -28,6 +28,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() export class SigninWithPasskeyApiService { + private logger: Logger; constructor( @Inject(DI.config) private config: Config, @@ -45,7 +46,9 @@ export class SigninWithPasskeyApiService { private rateLimiterService: RateLimiterService, private signinService: SigninService, private webAuthnService: WebAuthnService, + private loggerService: LoggerService, ) { + this.logger = this.loggerService.getLogger('PasskeyAuth'); } @bindThis @@ -82,8 +85,8 @@ export class SigninWithPasskeyApiService { }; try { - // not more than 1 attempt per second and not more than 500 attempts per hour - await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 500, minInterval: 1000 }, getIpHash(request.ip)); + // not more than 1 attempt per second and not more than 100 attempts per hour + await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 100, minInterval: 1000 }, getIpHash(request.ip)); } catch (err) { reply.code(429); return { @@ -107,14 +110,24 @@ export class SigninWithPasskeyApiService { } const context = body.context; - console.log(`passkey auth context: ${context}`); if (!context || typeof context !== 'string') { reply.code(400); return; } - const authorizedUserId: MiUser['id'] | null = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); - if (authorizedUserId == null) { + this.logger.debug(`VerifySignin Passkey auth: context: ${context}`); + let authorizedUserId : MiUser['id'] | null; + try { + authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); + } catch (err) { + this.logger.warn(`Verify error! : ${err}`); + const errorId = (err as IdentifiableError).id; + return error(403, { + id: errorId, + }); + } + + if (!authorizedUserId) { return error(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', }); From 0f5caa887e662b19628e02d013b9f99e1a88a0da Mon Sep 17 00:00:00 2001 From: Yuno Date: Thu, 12 Sep 2024 14:32:10 +0000 Subject: [PATCH 04/14] signinResponse should be placed under the SigninWithPasskeyResponse object. --- .../src/server/api/SigninWithPasskeyApiService.ts | 9 ++++++--- packages/misskey-js/etc/misskey-js.api.md | 3 ++- packages/misskey-js/src/entities.ts | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 6f21c45d595a..933abe2c159f 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -85,7 +85,7 @@ export class SigninWithPasskeyApiService { }; try { - // not more than 1 attempt per second and not more than 100 attempts per hour + // not more than 1 attempt per second and not more than 100 attempts per hour await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 100, minInterval: 1000 }, getIpHash(request.ip)); } catch (err) { reply.code(429); @@ -116,7 +116,7 @@ export class SigninWithPasskeyApiService { } this.logger.debug(`VerifySignin Passkey auth: context: ${context}`); - let authorizedUserId : MiUser['id'] | null; + let authorizedUserId: MiUser['id'] | null; try { authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); } catch (err) { @@ -159,6 +159,9 @@ export class SigninWithPasskeyApiService { }); } - return this.signinService.signin(request, reply, user); + const signinResponse = this.signinService.signin(request, reply, user); + return { + signinResponse: signinResponse, + }; } } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 30cbca5e434f..a5f12b41f469 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3045,7 +3045,8 @@ type SigninWithPasskeyRequest = { type SigninWithPasskeyResponse = { option?: object; context?: string; -} | SigninResponse; + signinResponse?: SigninResponse; +}; // @public (undocumented) type SignupPendingRequest = { diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index a648aafdea71..64ed90cbb119 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -279,7 +279,8 @@ export type SigninWithPasskeyRequest = { export type SigninWithPasskeyResponse = { option?: object; context?: string; -} | SigninResponse; + signinResponse?: SigninResponse; +}; export type SigninResponse = { id: User['id'], From 910e9239389fe440005c7a7d5764f304028102d4 Mon Sep 17 00:00:00 2001 From: Yuno Date: Thu, 12 Sep 2024 15:11:07 +0000 Subject: [PATCH 05/14] Frontend fix --- packages/frontend/src/components/MkSignin.vue | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 34d09ae85ccb..263cebc58a4a 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -75,6 +75,7 @@ import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; +import MkDivider from './MkDivider.vue'; import { query, extractDomain } from '@@/js/url.js'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; @@ -86,7 +87,6 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; -import MkDivider from './MkDivider.vue'; const signing = ref(false); const user = ref(null); @@ -166,11 +166,11 @@ function onPasskey(): void { signing.value = true; if (webAuthnSupported()) { misskeyApi('signin-with-passkey', {}) - .then((res) => { + .then((res: SigninWithPasskeyResponse) => { totpLogin.value = false; signing.value = false; queryingKey.value = true; - passkey_context.value = res.context; + passkey_context.value = res.context ?? ''; credentialRequest = parseRequestOptionsFromJSON({ publicKey: res.option, }); @@ -198,15 +198,8 @@ async function queryPasskey(): Promise { context: passkey_context.value, }); }).then((res: SigninWithPasskeyResponse) => { - emit('login', res); - return onLogin(res); - }).catch(err => { - if (err === null) return; - os.alert({ - type: 'error', - text: i18n.ts.signinFailed, - }); - signing.value = false; + emit('login', res.signinResponse); + return onLogin(res.signinResponse); }); } From eeb55de6977b39f52634f13e27f60c4f35804151 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Fri, 13 Sep 2024 00:22:35 +0000 Subject: [PATCH 06/14] Fix: Rate limiting key for passkey signin Use specific rate limiting key: 'signin-with-passkey' for passkey sign-in API to avoid collisions with signin rate-limit. --- packages/backend/src/server/api/SigninWithPasskeyApiService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 933abe2c159f..8de5ed96c631 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -86,7 +86,7 @@ export class SigninWithPasskeyApiService { try { // not more than 1 attempt per second and not more than 100 attempts per hour - await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 100, minInterval: 1000 }, getIpHash(request.ip)); + await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 60 * 1000, max: 100, minInterval: 1000 }, getIpHash(request.ip)); } catch (err) { reply.code(429); return { From 51000554b061b069cf1530de885ead6648864b64 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Fri, 13 Sep 2024 02:50:12 +0000 Subject: [PATCH 07/14] Refactor: enhance Passkey sign-in flow and error handling - Increased the rate limit for Passkey sign-in attempts to accommodate the two API calls needed per sign-in. - Improved error messages and handling in both the `WebAuthnService` and the `SigninWithPasskeyApiService`, providing more context and better usability. - Updated error messages to provide more specific and helpful details to the user. These changes aim to enhance the Passkey sign-in experience by providing more robust error handling, improving security by limiting API calls, and delivering a more user-friendly interface. --- packages/backend/src/core/WebAuthnService.ts | 9 +++---- .../server/api/SigninWithPasskeyApiService.ts | 27 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index c22769aa14fd..a75ef73ff482 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -181,14 +181,14 @@ export class WebAuthnService { /** * Verify Webauthn AuthenticationCredential * @throws IdentifiableError - * @returns MiUser['id'] or null + * @returns If the challenge is successful, return the user ID. Otherwise, return null. */ @bindThis public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise { const challenge = await this.redisClient.get(`webauthn:challenge:${context}`); if (!challenge) { - throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found'); + throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`); } await this.redisClient.del(`webauthn:challenge:${context}`); @@ -198,7 +198,7 @@ export class WebAuthnService { }); if (!key) { - throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'unknown key'); + throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key'); } const relyingParty = await this.getRelyingParty(); @@ -219,8 +219,7 @@ export class WebAuthnService { requireUserVerification: true, }); } catch (error) { - console.error(error); - throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed'); + throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`); } const { verified, authenticationInfo } = verification; diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 8de5ed96c631..4751f8b5a5b9 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -85,8 +85,9 @@ export class SigninWithPasskeyApiService { }; try { - // not more than 1 attempt per second and not more than 100 attempts per hour - await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 60 * 1000, max: 100, minInterval: 1000 }, getIpHash(request.ip)); + // Not more than 1 API call per 250ms and not more than 100 attempts per 30min + // NOTE: 1 Sign-in require 2 API calls + await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip)); } catch (err) { reply.code(429); return { @@ -98,29 +99,32 @@ export class SigninWithPasskeyApiService { }; } - // Initiate Passkey Auth with context + // Initiate Passkey Auth challenge with context if (!credential) { const context = randomUUID(); - const authRequest = { + this.logger.info(`Initiate Passkey challenge: context: ${context}`); + const authChallengeOptions = { option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context), context: context, }; reply.code(200); - return authRequest; + return authChallengeOptions; } const context = body.context; if (!context || typeof context !== 'string') { - reply.code(400); - return; + return error(400, { + id: '1658cc2e-4495-461f-aee4-d403cdf073c1', + }); } - this.logger.debug(`VerifySignin Passkey auth: context: ${context}`); + this.logger.debug(`Try Sign-in with Passkey: context: ${context}`); + let authorizedUserId: MiUser['id'] | null; try { authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); } catch (err) { - this.logger.warn(`Verify error! : ${err}`); + this.logger.warn(`Passkey challenge Verify error! : ${err}`); const errorId = (err as IdentifiableError).id; return error(403, { id: errorId, @@ -141,7 +145,7 @@ export class SigninWithPasskeyApiService { if (user == null) { return error(403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + id: '652f899f-66d4-490e-993e-6606c8ec04c3', }); } @@ -153,9 +157,10 @@ export class SigninWithPasskeyApiService { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + // Authentication was successful, but passwordless login is not enabled if (!profile.usePasswordLessLogin) { return await fail(user.id, 403, { - id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912', }); } From e8ffef790258ddf3446756412b96547576163da0 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Fri, 13 Sep 2024 04:04:00 +0000 Subject: [PATCH 08/14] Refactor: Streamline 2FA flow and remove redundant Passkey button. - Separate the flow of 1FA and 2FA. - Remove duplicate passkey buttons --- packages/frontend/src/components/MkSignin.vue | 16 +++++++--------- .../frontend/src/components/MkSigninDialog.vue | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 263cebc58a4a..d4c41cfc89b0 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -45,10 +45,6 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts.or }}

- - - - @@ -57,13 +53,15 @@ SPDX-License-Identifier: AGPL-3.0-only {{ signing ? i18n.ts.loggingIn : i18n.ts.login }}
-
- - +
+

{{ i18n.ts.or }}

+
+
+ {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} -

{{ i18n.ts.useSecurityKey }}

+

{{ i18n.ts.useSecurityKey }}

diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 524c62b4d3aa..d48780e9de6d 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only From 967b6e26ec2634a37e349a8cd3b0926bc49652f5 Mon Sep 17 00:00:00 2001 From: Squarecat-meow Date: Sun, 15 Sep 2024 16:49:59 +0000 Subject: [PATCH 09/14] Fix: Add error messages to MkSignin --- locales/en-US.yml | 5 +++- locales/index.d.ts | 12 ++++++++++ locales/ja-JP.yml | 3 +++ locales/ko-KR.yml | 3 +++ packages/frontend/src/components/MkSignin.vue | 24 +++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 6ffc4d3d1c12..354cfe3599a6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1263,7 +1263,10 @@ confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?" createdLists: "Created lists" createdAntennas: "Created antennas" -signinWithPasskey: "Login With Passkey" +signinWithPasskey: "Login with passkey" +unknownWebAuthnKey: "It is not authenticated passkey." +verificationFailed: "Failed to verificate passkey." +passwordlessLoginDisabled: "Passkey verification successful, but the passwordless login was not enabled." _delivery: status: "Delivery status" diff --git a/locales/index.d.ts b/locales/index.d.ts index aff027811fbd..c19207552182 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5112,6 +5112,18 @@ export interface Locale extends ILocale { * パスキーでログイン */ "signinWithPasskey": string; + /** + * 登録していないパスキーです。 + */ + "unknownWebAuthnKey": string; + /** + * パスキー検証に失敗しました。 + */ + "verificationFailed": string; + /** + * パスキー検証には成功しましたが、パスワードレスログインが無効にしています。 + */ + "passwordlessLoginDisabled": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2af3d62392a0..e0e78e3b6c8c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1274,6 +1274,9 @@ modified: "変更あり" discard: "破棄" thereAreNChanges: "{n}件の変更があります" signinWithPasskey: "パスキーでログイン" +unknownWebAuthnKey: "登録していないパスキーです。" +verificationFailed: "パスキー検証に失敗しました。" +passwordlessLoginDisabled: "パスキー検証には成功しましたが、パスワードレスログインが無効にしています。" _delivery: status: "配信状態" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index ca812113f398..cbb70336a2b7 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1250,6 +1250,9 @@ inquiry: "문의하기" tryAgain: "다시 시도해 주세요." confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인" signinWithPasskey: "패스키로 로그인" +unknownWebAuthnKey: "등록되지 않은 패스키 입니다." +verificationFailed: "패스키 검증이 실패했습니다." +passwordlessLoginDisabled: "인증에는 성공했지만, 비밀번호 없이 로그인 설정이 활성화 되어있지 않습니다." _delivery: status: "전송 상태" diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index d4c41cfc89b0..e0bfe5217cc4 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -263,6 +263,30 @@ function loginFailed(err: any): void { }); break; } + case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.unknownWebAuthnKey, + }); + break; + } + case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.verificationFailed, + }); + break; + } + case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passwordlessLoginDisabled, + }); + break; + } default: { console.error(err); os.alert({ From d76dc7e3b920322ad1c34a01f666aa698118d7a7 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Sep 2024 06:41:09 +0000 Subject: [PATCH 10/14] chore: Hide passkey button if the entered user does not use passkey login --- packages/frontend/src/components/MkSignin.vue | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index e0bfe5217cc4..6877a0e847e4 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -53,15 +53,15 @@ SPDX-License-Identifier: AGPL-3.0-only {{ signing ? i18n.ts.loggingIn : i18n.ts.login }} -
+

{{ i18n.ts.or }}

-
+
{{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} -

{{ i18n.ts.useSecurityKey }}

+

{{ i18n.ts.useSecurityKey }}

@@ -73,14 +73,14 @@ import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; -import MkDivider from './MkDivider.vue'; import { query, extractDomain } from '@@/js/url.js'; +import { host as configHost } from '@@/js/config.js'; +import MkDivider from './MkDivider.vue'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { host as configHost } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; @@ -88,6 +88,7 @@ import { i18n } from '@/i18n.js'; const signing = ref(false); const user = ref(null); +const usePasswordLessLogin = ref(true); const username = ref(''); const password = ref(''); const token = ref(''); @@ -118,8 +119,10 @@ function onUsernameChange(): void { username: username.value, }).then(userResponse => { user.value = userResponse; + usePasswordLessLogin.value = userResponse.usePasswordLessLogin; }, () => { user.value = null; + usePasswordLessLogin.value = true; }); } From 95ad81f1ec060e6b56a42445dc47700d0d911d00 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Sep 2024 06:47:17 +0000 Subject: [PATCH 11/14] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60712fbf4e71..9c7b5dfbfcef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### General - Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) - Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように +- Feat: パスキーでログインボタンを実装 (#14574) ### Client - Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 From 61d077abc92c80f58112b22b1078e3c913662cf0 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Sep 2024 08:12:38 +0000 Subject: [PATCH 12/14] Refactor: Rename functions and Add comments --- packages/backend/src/core/WebAuthnService.ts | 4 ++++ .../server/api/SigninWithPasskeyApiService.ts | 1 + packages/frontend/src/components/MkSignin.vue | 17 ++++++++--------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index a75ef73ff482..75ab0a207c45 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -164,6 +164,10 @@ export class WebAuthnService { return authenticationOptions; } + /** + * Initiate Passkey Auth (Without specifying user) + * @returns authenticationOptions + */ @bindThis public async initiateSignInWithPasskeyAuthentication(context: string): Promise { const relyingParty = await this.getRelyingParty(); diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 4751f8b5a5b9..9ba23c54e226 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -113,6 +113,7 @@ export class SigninWithPasskeyApiService { const context = body.context; if (!context || typeof context !== 'string') { + // If try Authentication without context return error(400, { id: '1658cc2e-4495-461f-aee4-d403cdf073c1', }); diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 6877a0e847e4..971a70b3d7a8 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} @@ -97,6 +97,7 @@ const totpLogin = ref(false); const isBackupCode = ref(false); const queryingKey = ref(false); let credentialRequest: CredentialRequestOptions | null = null; +const passkey_context = ref(''); const emit = defineEmits<{ (ev: 'login', v: any): void; @@ -132,7 +133,7 @@ function onLogin(res: any): Promise | void { } } -async function queryKey(): Promise { +async function query2FaKey(): Promise { if (credentialRequest == null) return; queryingKey.value = true; await webAuthnRequest(credentialRequest) @@ -161,9 +162,7 @@ async function queryKey(): Promise { }); } -const passkey_context = ref(''); - -function onPasskey(): void { +function onPasskeyLogin(): void { signing.value = true; if (webAuthnSupported()) { misskeyApi('signin-with-passkey', {}) @@ -186,8 +185,8 @@ async function queryPasskey(): Promise { queryingKey.value = true; console.log('Waiting passkey auth...'); await webAuthnRequest(credentialRequest) - .catch((er) => { - console.warn('Fail!!', er); + .catch((err) => { + console.warn('Passkey Auth fail!: ', err); queryingKey.value = false; return Promise.reject(null); }).then(credential => { @@ -218,7 +217,7 @@ function onSubmit(): void { publicKey: res, }); }) - .then(() => queryKey()) + .then(() => query2FaKey()) .catch(loginFailed); } else { totpLogin.value = true; From 3189266c1ed3fb398e1e054bd68053955e3b095e Mon Sep 17 00:00:00 2001 From: Yuri Lee Date: Wed, 25 Sep 2024 19:56:44 +0900 Subject: [PATCH 13/14] Update locales/ja-JP.yml Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- locales/ja-JP.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e0e78e3b6c8c..f04a8baefbe5 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1274,9 +1274,9 @@ modified: "変更あり" discard: "破棄" thereAreNChanges: "{n}件の変更があります" signinWithPasskey: "パスキーでログイン" -unknownWebAuthnKey: "登録していないパスキーです。" -verificationFailed: "パスキー検証に失敗しました。" -passwordlessLoginDisabled: "パスキー検証には成功しましたが、パスワードレスログインが無効にしています。" +unknownWebAuthnKey: "登録されていないパスキーです。" +passkeyVerificationFailed: "パスキーの検証に失敗しました。" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" _delivery: status: "配信状態" From e6e9df05843b22fd602ad55a28b6099b6fbaf76a Mon Sep 17 00:00:00 2001 From: Yuno Date: Wed, 25 Sep 2024 20:06:48 +0900 Subject: [PATCH 14/14] Fix: Update translation - update index.d.ts - update ko-KR.yml, en-US.yml - Fix: Reflect Changed i18n key on MkSignin --- locales/en-US.yml | 4 ++-- locales/index.d.ts | 10 +++++----- locales/ko-KR.yml | 4 ++-- packages/frontend/src/components/MkSignin.vue | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 354cfe3599a6..986cf07e1495 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1265,8 +1265,8 @@ createdLists: "Created lists" createdAntennas: "Created antennas" signinWithPasskey: "Login with passkey" unknownWebAuthnKey: "It is not authenticated passkey." -verificationFailed: "Failed to verificate passkey." -passwordlessLoginDisabled: "Passkey verification successful, but the passwordless login was not enabled." +passkeyVerificationFailed: "Failed to verificate passkey." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification successful, but the passwordless login was not enabled." _delivery: status: "Delivery status" diff --git a/locales/index.d.ts b/locales/index.d.ts index c19207552182..93d900af378f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5113,17 +5113,17 @@ export interface Locale extends ILocale { */ "signinWithPasskey": string; /** - * 登録していないパスキーです。 + * 登録されていないパスキーです。 */ "unknownWebAuthnKey": string; /** - * パスキー検証に失敗しました。 + * パスキーの検証に失敗しました。 */ - "verificationFailed": string; + "passkeyVerificationFailed": string; /** - * パスキー検証には成功しましたが、パスワードレスログインが無効にしています。 + * パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。 */ - "passwordlessLoginDisabled": string; + "passkeyVerificationSucceededButPasswordlessLoginDisabled": string; "_delivery": { /** * 配信状態 diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index cbb70336a2b7..78d0b4b98e74 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1251,8 +1251,8 @@ tryAgain: "다시 시도해 주세요." confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인" signinWithPasskey: "패스키로 로그인" unknownWebAuthnKey: "등록되지 않은 패스키 입니다." -verificationFailed: "패스키 검증이 실패했습니다." -passwordlessLoginDisabled: "인증에는 성공했지만, 비밀번호 없이 로그인 설정이 활성화 되어있지 않습니다." +passkeyVerificationFailed: "패스키 검증이 실패했습니다." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "인증에는 성공했지만, 비밀번호 없이 로그인 설정이 활성화 되어있지 않습니다." _delivery: status: "전송 상태" diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 971a70b3d7a8..7942a84d66f3 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -277,7 +277,7 @@ function loginFailed(err: any): void { os.alert({ type: 'error', title: i18n.ts.loginFailed, - text: i18n.ts.verificationFailed, + text: i18n.ts.passkeyVerificationFailed, }); break; } @@ -285,7 +285,7 @@ function loginFailed(err: any): void { os.alert({ type: 'error', title: i18n.ts.loginFailed, - text: i18n.ts.passwordlessLoginDisabled, + text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled, }); break; }