From cede212815f686338dcba5b9de7ce39a9b120acd Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:22:41 +0900 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=E3=83=91=E3=82=B9=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=83=AC=E3=82=B9=E3=83=AD=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=81=8C=E6=9C=89=E5=8A=B9=E3=81=AB=E3=81=AA=E3=81=A3?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=82=8B=E5=A0=B4=E5=90=88=E3=81=A7=E3=82=82?= =?UTF-8?q?=E8=AA=A4=E3=81=A3=E3=81=A6=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=82=92=E8=A6=81=E6=B1=82=E3=81=97=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 4 +++ locales/ja-JP.yml | 1 + .../src/server/api/SigninApiService.ts | 11 ++++++ .../src/components/MkSignin.input.vue | 19 +++++----- packages/frontend/src/components/MkSignin.vue | 36 +++++++++++++++++-- packages/misskey-js/etc/misskey-js.api.md | 1 + packages/misskey-js/src/entities.ts | 1 + 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index f0dead1245..c21d89e8f8 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5158,6 +5158,10 @@ export interface Locale extends ILocale { * パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。 */ "passkeyVerificationSucceededButPasswordlessLoginDisabled": string; + /** + * お使いのブラウザはパスキーをサポートしていません。 + */ + "yourBrowserDoesNotSupportPasskey": string; /** * フォロワーへのメッセージ */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0076c467ec..3ee6ab1cee 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1285,6 +1285,7 @@ signinWithPasskey: "パスキーでログイン" unknownWebAuthnKey: "登録されていないパスキーです。" passkeyVerificationFailed: "パスキーの検証に失敗しました。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" +yourBrowserDoesNotSupportPasskey: "お使いのブラウザはパスキーをサポートしていません。" messageToFollower: "フォロワーへのメッセージ" target: "対象" diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 0d24ffa56a..d608ad527e 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -136,6 +136,17 @@ export class SigninApiService { if (password == null) { reply.code(200); if (profile.twoFactorEnabled) { + if (profile.usePasswordLessLogin && securityKeysAvailable) { + const authRequest = await this.webAuthnService.initiateAuthentication(user.id); + + return { + finished: false, + next: 'passkey', + force: true, + authRequest, + } satisfies Misskey.entities.SigninFlowResponse; + } + return { finished: false, next: 'password', diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue index 34c22abc31..da54a0967f 100644 --- a/packages/frontend/src/components/MkSignin.input.vue +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -40,14 +40,16 @@ SPDX-License-Identifier: AGPL-3.0-only </form> <!-- パスワードレスログイン --> - <div :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - <div> - <MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)"> - <i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }} - </MkButton> - </div> + <template v-if="webAuthnSupported()"> + <div :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + <div> + <MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)"> + <i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }} + </MkButton> + </div> + </template> </div> </div> </template> @@ -55,6 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { ref } from 'vue'; import { toUnicode } from 'punycode/'; +import { supported as webAuthnSupported } from '@github/webauthn-json/browser-ponyfill'; import { query, extractDomain } from '@@/js/url.js'; import { host as configHost } from '@@/js/config.js'; diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index a773cefdab..2a6d19b6c5 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only key="passkey" :credentialRequest="credentialRequest!" - :isPerformingPasswordlessLogin="doingPasskeyFromInputPage" + :isPerformingPasswordlessLogin="doingPasskeyFromInputPage || needForcedPasskey" @done="onPasskeyDone" @useTotp="onUseTotp" @@ -101,6 +101,7 @@ const waiting = ref(false); const passwordPageEl = useTemplateRef('passwordPageEl'); const needCaptcha = ref(false); +const needForcedPasskey = ref(false); const userInfo = ref<null | Misskey.entities.UserDetailed>(null); const password = ref(''); @@ -247,7 +248,19 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi break; } case 'passkey': { - if (webAuthnSupported()) { + if (res.force === true) { + if (webAuthnSupported()) { + needForcedPasskey.value = true; + credentialRequest.value = parseRequestOptionsFromJSON({ + publicKey: res.authRequest, + }); + page.value = 'passkey'; + } else { + throw { + id: '8b12bdf5-d5ed-4429-b5da-e3370cfcb869', + }; + } + } else if (webAuthnSupported()) { credentialRequest.value = parseRequestOptionsFromJSON({ publicKey: res.authRequest, }); @@ -264,6 +277,9 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi page.value = 'input'; password.value = ''; } + if (!('force' in res)) { + needForcedPasskey.value = false; + } passwordPageEl.value?.resetCaptcha(); nextTick(() => { waiting.value = false; @@ -286,6 +302,7 @@ function onSigninApiError(err?: any): void { const id = err?.id ?? null; switch (id) { + // signin-flow api case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { os.alert({ type: 'error', @@ -338,6 +355,8 @@ function onSigninApiError(err?: any): void { }); break; } + + // signin-with-passkey api case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { os.alert({ type: 'error', @@ -354,6 +373,18 @@ function onSigninApiError(err?: any): void { }); break; } + + // client-produced error + case '8b12bdf5-d5ed-4429-b5da-e3370cfcb869': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.yourBrowserDoesNotSupportPasskey, + }); + break; + } + + // default default: { console.error(err); os.alert({ @@ -369,6 +400,7 @@ function onSigninApiError(err?: any): void { page.value = 'input'; password.value = ''; } + needForcedPasskey.value = false; passwordPageEl.value?.resetCaptcha(); nextTick(() => { waiting.value = false; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 72c236373d..37342db5c1 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3078,6 +3078,7 @@ type SigninFlowResponse = { } | { finished: false; next: 'passkey'; + force?: boolean; authRequest: PublicKeyCredentialRequestOptionsJSON; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index dd88791ed0..39479df33b 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -294,6 +294,7 @@ export type SigninFlowResponse = { } | { finished: false; next: 'passkey'; + force?: boolean; authRequest: PublicKeyCredentialRequestOptionsJSON; };