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; };