feat: passkey support (#11804)

https://github.com/MisskeyIO/misskey/pull/149
This commit is contained in:
syuilo
2023-09-08 14:05:03 +09:00
committed by GitHub
parent bc52d7a4fb
commit ff9a65e8fa
30 changed files with 800 additions and 1028 deletions

View File

@@ -38,16 +38,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.securityKeyAndPasskey }}</template>
<div class="_gaps_s">
<MkInfo>
{{ i18n.ts._2fa.securityKeyInfo }}<br>
<br>
{{ i18n.ts._2fa.chromePasskeyNotSupported }}
{{ i18n.ts._2fa.securityKeyInfo }}
</MkInfo>
<MkInfo v-if="!supportsCredentials" warn>
<MkInfo v-if="!webAuthnSupported()" warn>
{{ i18n.ts._2fa.securityKeyNotSupported }}
</MkInfo>
<MkInfo v-else-if="supportsCredentials && !$i.twoFactorEnabled" warn>
<MkInfo v-else-if="webAuthnSupported() && !$i.twoFactorEnabled" warn>
{{ i18n.ts._2fa.registerTOTPBeforeKey }}
</MkInfo>
@@ -75,8 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, defineAsyncComponent } from 'vue';
import { hostname } from '@/config';
import { byteify, hexify, stringify } from '@/scripts/2fa';
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -94,10 +91,9 @@ withDefaults(defineProps<{
first: false,
});
const supportsCredentials = ref(!!navigator.credentials);
const usePasswordLessLogin = $computed(() => $i!.usePasswordLessLogin);
const usePasswordLessLogin = $computed(() => $i?.usePasswordLessLogin ?? false);
async function registerTOTP() {
async function registerTOTP(): Promise<void> {
const password = await os.inputText({
title: i18n.ts._2fa.registerTOTP,
text: i18n.ts._2fa.passwordToTOTP,
@@ -115,7 +111,7 @@ async function registerTOTP() {
}, {}, 'closed');
}
function unregisterTOTP() {
function unregisterTOTP(): void {
os.inputText({
title: i18n.ts.password,
type: 'password',
@@ -133,7 +129,7 @@ function unregisterTOTP() {
});
}
function renewTOTP() {
function renewTOTP(): void {
os.confirm({
type: 'question',
title: i18n.ts._2fa.renewTOTP,
@@ -192,8 +188,10 @@ async function addSecurityKey() {
});
if (password.canceled) return;
const challenge: any = await os.apiWithDialog('i/2fa/register-key', {
password: password.result,
const registrationOptions = parseCreationOptionsFromJSON({
publicKey: await os.apiWithDialog('i/2fa/register-key', {
password: password.result,
}),
});
const name = await os.inputText({
@@ -205,26 +203,8 @@ async function addSecurityKey() {
});
if (name.canceled) return;
const webAuthnCreation = navigator.credentials.create({
publicKey: {
challenge: byteify(challenge.challenge, 'base64'),
rp: {
id: hostname,
name: 'Misskey',
},
user: {
id: byteify($i!.id, 'ascii'),
name: $i!.username,
displayName: $i!.name,
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
timeout: 60000,
attestation: 'direct',
},
}) as Promise<PublicKeyCredential & { response: AuthenticatorAttestationResponse; } | null>;
const credential = await os.promiseDialog(
webAuthnCreation,
webAuthnCreate(registrationOptions),
null,
() => {}, // ユーザーのキャンセルはrejectなのでエラーダイアログを出さない
i18n.ts._2fa.tapSecurityKey,
@@ -234,10 +214,7 @@ async function addSecurityKey() {
await os.apiWithDialog('i/2fa/key-done', {
password: password.result,
name: name.result,
challengeId: challenge.challengeId,
// we convert each 16 bits to a string to serialise
clientDataJSON: stringify(credential.response.clientDataJSON),
attestationObject: hexify(credential.response.attestationObject),
credential: credential.toJSON(),
});
}