Compare commits
5 Commits
248dac29a0
...
7f9bec8b7b
Author | SHA1 | Date |
---|---|---|
かっこかり | 7f9bec8b7b | |
かっこかり | 373d2aeb04 | |
かっこかり | 4fce4e4b08 | |
kakkokari-gtyih | 51098f4389 | |
kakkokari-gtyih | cede212815 |
|
@ -5174,6 +5174,10 @@ export interface Locale extends ILocale {
|
||||||
* パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。
|
* パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。
|
||||||
*/
|
*/
|
||||||
"passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
|
"passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
|
||||||
|
/**
|
||||||
|
* お使いのブラウザはパスキーをサポートしていません。
|
||||||
|
*/
|
||||||
|
"yourBrowserDoesNotSupportPasskey": string;
|
||||||
/**
|
/**
|
||||||
* フォロワーへのメッセージ
|
* フォロワーへのメッセージ
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1289,6 +1289,7 @@ signinWithPasskey: "パスキーでログイン"
|
||||||
unknownWebAuthnKey: "登録されていないパスキーです。"
|
unknownWebAuthnKey: "登録されていないパスキーです。"
|
||||||
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
|
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
|
||||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
||||||
|
yourBrowserDoesNotSupportPasskey: "お使いのブラウザはパスキーをサポートしていません。"
|
||||||
messageToFollower: "フォロワーへのメッセージ"
|
messageToFollower: "フォロワーへのメッセージ"
|
||||||
target: "対象"
|
target: "対象"
|
||||||
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
|
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
|
||||||
|
|
|
@ -137,6 +137,17 @@ export class SigninApiService {
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
reply.code(200);
|
reply.code(200);
|
||||||
if (profile.twoFactorEnabled) {
|
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 {
|
return {
|
||||||
finished: false,
|
finished: false,
|
||||||
next: 'password',
|
next: 'password',
|
||||||
|
|
|
@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- パスワードレスログイン -->
|
<!-- パスワードレスログイン -->
|
||||||
|
<template v-if="webAuthnSupported()">
|
||||||
<div :class="$style.orHr">
|
<div :class="$style.orHr">
|
||||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
|
<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -55,6 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
|
import { supported as webAuthnSupported } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
|
||||||
import { query, extractDomain } from '@@/js/url.js';
|
import { query, extractDomain } from '@@/js/url.js';
|
||||||
import { host as configHost } from '@@/js/config.js';
|
import { host as configHost } from '@@/js/config.js';
|
||||||
|
|
|
@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
key="passkey"
|
key="passkey"
|
||||||
|
|
||||||
:credentialRequest="credentialRequest!"
|
:credentialRequest="credentialRequest!"
|
||||||
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
|
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage || needForcedPasskey"
|
||||||
|
|
||||||
@done="onPasskeyDone"
|
@done="onPasskeyDone"
|
||||||
@useTotp="onUseTotp"
|
@useTotp="onUseTotp"
|
||||||
|
@ -100,6 +100,7 @@ const waiting = ref(false);
|
||||||
|
|
||||||
const passwordPageEl = useTemplateRef('passwordPageEl');
|
const passwordPageEl = useTemplateRef('passwordPageEl');
|
||||||
const needCaptcha = ref(false);
|
const needCaptcha = ref(false);
|
||||||
|
const needForcedPasskey = ref(false);
|
||||||
|
|
||||||
const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
|
const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
@ -247,7 +248,22 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'passkey': {
|
case 'passkey': {
|
||||||
|
if (res.force === true) {
|
||||||
if (webAuthnSupported()) {
|
if (webAuthnSupported()) {
|
||||||
|
needForcedPasskey.value = true;
|
||||||
|
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||||
|
publicKey: res.authRequest,
|
||||||
|
});
|
||||||
|
page.value = 'passkey';
|
||||||
|
} else {
|
||||||
|
const err = {
|
||||||
|
id: '8b12bdf5-d5ed-4429-b5da-e3370cfcb869',
|
||||||
|
};
|
||||||
|
|
||||||
|
onSigninApiError(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
} else if (webAuthnSupported()) {
|
||||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||||
publicKey: res.authRequest,
|
publicKey: res.authRequest,
|
||||||
});
|
});
|
||||||
|
@ -264,6 +280,9 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi
|
||||||
page.value = 'input';
|
page.value = 'input';
|
||||||
password.value = '';
|
password.value = '';
|
||||||
}
|
}
|
||||||
|
if (!('force' in res)) {
|
||||||
|
needForcedPasskey.value = false;
|
||||||
|
}
|
||||||
passwordPageEl.value?.resetCaptcha();
|
passwordPageEl.value?.resetCaptcha();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
waiting.value = false;
|
waiting.value = false;
|
||||||
|
@ -286,6 +305,7 @@ function onSigninApiError(err?: any): void {
|
||||||
const id = err?.id ?? null;
|
const id = err?.id ?? null;
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
|
// signin-flow api
|
||||||
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@ -338,6 +358,8 @@ function onSigninApiError(err?: any): void {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signin-with-passkey api
|
||||||
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@ -354,6 +376,18 @@ function onSigninApiError(err?: any): void {
|
||||||
});
|
});
|
||||||
break;
|
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: {
|
default: {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
os.alert({
|
os.alert({
|
||||||
|
@ -369,6 +403,7 @@ function onSigninApiError(err?: any): void {
|
||||||
page.value = 'input';
|
page.value = 'input';
|
||||||
password.value = '';
|
password.value = '';
|
||||||
}
|
}
|
||||||
|
needForcedPasskey.value = false;
|
||||||
passwordPageEl.value?.resetCaptcha();
|
passwordPageEl.value?.resetCaptcha();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
waiting.value = false;
|
waiting.value = false;
|
||||||
|
|
|
@ -3083,6 +3083,7 @@ type SigninFlowResponse = {
|
||||||
} | {
|
} | {
|
||||||
finished: false;
|
finished: false;
|
||||||
next: 'passkey';
|
next: 'passkey';
|
||||||
|
force?: boolean;
|
||||||
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -294,6 +294,7 @@ export type SigninFlowResponse = {
|
||||||
} | {
|
} | {
|
||||||
finished: false;
|
finished: false;
|
||||||
next: 'passkey';
|
next: 'passkey';
|
||||||
|
force?: boolean;
|
||||||
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue