diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-09-22 14:12:33 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-09-22 14:12:33 +0900 |
| commit | c836157edb869e80b15f51bb8f48725e3b898b9a (patch) | |
| tree | c275a865b697afa4c5d045d16b1ca8999999f9cf /packages/frontend/src | |
| parent | tweak ui (diff) | |
| download | misskey-c836157edb869e80b15f51bb8f48725e3b898b9a.tar.gz misskey-c836157edb869e80b15f51bb8f48725e3b898b9a.tar.bz2 misskey-c836157edb869e80b15f51bb8f48725e3b898b9a.zip | |
enhance: 二要素認証設定時のセキュリティを強化 (#11863)
* enhance: 二要素認証設定時のセキュリティを強化
パスワード入力が必要な操作を行う際、二要素認証が有効であれば確認コードの入力も必要にする
* Update CoreModule.ts
* Update 2fa.ts
* wip
* wip
* Update 2fa.ts
* tweak
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/components/MkInput.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPasswordDialog.vue | 70 | ||||
| -rw-r--r-- | packages/frontend/src/os.ts | 13 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/2fa.vue | 65 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/email.vue | 20 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/other.vue | 10 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/security.vue | 29 |
7 files changed, 140 insertions, 71 deletions
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index e9397ce86f..315ce958c5 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -155,6 +155,10 @@ onMounted(() => { } }); }); + +defineExpose({ + focus, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue new file mode 100644 index 0000000000..afb4929fcf --- /dev/null +++ b/packages/frontend/src/components/MkPasswordDialog.vue @@ -0,0 +1,70 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :width="370" + :height="400" + @close="onClose" + @closed="emit('closed')" +> + <template #header>{{ i18n.ts.authentication }}</template> + + <MkSpacer :marginMin="20" :marginMax="28"> + <div style="padding: 0 0 16px 0; text-align: center;"> + <i class="ti ti-lock" style="font-size: 32px; color: var(--accent);"></i> + <div style="margin-top: 10px;">{{ i18n.ts.authenticationRequiredToContinue }}</div> + </div> + + <div class="_gaps"> + <MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true"> + <template #prefix><i class="ti ti-password"></i></template> + </MkInput> + + <MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false"> + <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> + <template #prefix><i class="ti ti-123"></i></template> + </MkInput> + + <MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton> + </div> + </MkSpacer> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { onMounted } from 'vue'; +import MkInput from '@/components/MkInput.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import { i18n } from '@/i18n.js'; +import { $i } from '@/account.js'; + +const emit = defineEmits<{ + (ev: 'done', v: { password: string; token: string | null; }): void; + (ev: 'closed'): void; + (ev: 'cancelled'): void; +}>(); + +const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); +const passwordInput = $shallowRef<InstanceType<typeof MkInput>>(); +const password = $ref(''); +const token = $ref(null); + +function onClose() { + emit('cancelled'); + if (dialog) dialog.close(); +} + +function done(res) { + emit('done', { password, token }); + if (dialog) dialog.close(); +} + +onMounted(() => { + if (passwordInput) passwordInput.focus(); +}); +</script> diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 4cd1c3ef31..8aed5797e1 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -17,6 +17,7 @@ import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; import MkPageWindow from '@/components/MkPageWindow.vue'; import MkToast from '@/components/MkToast.vue'; import MkDialog from '@/components/MkDialog.vue'; +import MkPasswordDialog from '@/components/MkPasswordDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; @@ -333,6 +334,18 @@ export function inputDate(props: { }); } +export function authenticateDialog(): Promise<{ canceled: true; result: undefined; } | { + canceled: false; result: { password: string; token: string | null; }; +}> { + return new Promise((resolve, reject) => { + popup(MkPasswordDialog, {}, { + done: result => { + resolve(result ? { canceled: false, result } : { canceled: true, result: undefined }); + }, + }, 'closed'); + }); +} + export function select<C = any>(props: { title?: string | null; text?: string | null; diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 37455ac2d0..8a89a3a86d 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -94,16 +94,12 @@ withDefaults(defineProps<{ const usePasswordLessLogin = $computed(() => $i?.usePasswordLessLogin ?? false); async function registerTOTP(): Promise<void> { - const password = await os.inputText({ - title: i18n.ts._2fa.registerTOTP, - text: i18n.ts._2fa.passwordToTOTP, - type: 'password', - autocomplete: 'current-password', - }); - if (password.canceled) return; + const auth = await os.authenticateDialog(); + if (auth.canceled) return; const twoFactorData = await os.apiWithDialog('i/2fa/register', { - password: password.result, + password: auth.result.password, + token: auth.result.token, }); os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), { @@ -111,20 +107,17 @@ async function registerTOTP(): Promise<void> { }, {}, 'closed'); } -function unregisterTOTP(): void { - os.inputText({ - title: i18n.ts.password, - type: 'password', - autocomplete: 'current-password', - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.apiWithDialog('i/2fa/unregister', { - password: password, - }).catch(error => { - os.alert({ - type: 'error', - text: error, - }); +async function unregisterTOTP(): Promise<void> { + const auth = await os.authenticateDialog(); + if (auth.canceled) return; + + os.apiWithDialog('i/2fa/unregister', { + password: auth.result.password, + token: auth.result.token, + }).catch(error => { + os.alert({ + type: 'error', + text: error, }); }); } @@ -150,15 +143,12 @@ async function unregisterKey(key) { }); if (confirm.canceled) return; - const password = await os.inputText({ - title: i18n.ts.password, - type: 'password', - autocomplete: 'current-password', - }); - if (password.canceled) return; + const auth = await os.authenticateDialog(); + if (auth.canceled) return; await os.apiWithDialog('i/2fa/remove-key', { - password: password.result, + password: auth.result.password, + token: auth.result.token, credentialId: key.id, }); os.success(); @@ -181,16 +171,13 @@ async function renameKey(key) { } async function addSecurityKey() { - const password = await os.inputText({ - title: i18n.ts.password, - type: 'password', - autocomplete: 'current-password', - }); - if (password.canceled) return; + const auth = await os.authenticateDialog(); + if (auth.canceled) return; const registrationOptions = parseCreationOptionsFromJSON({ publicKey: await os.apiWithDialog('i/2fa/register-key', { - password: password.result, + password: auth.result.password, + token: auth.result.token, }), }); @@ -211,8 +198,12 @@ async function addSecurityKey() { ); if (!credential) return; + const auth2 = await os.authenticateDialog(); + if (auth2.canceled) return; + await os.apiWithDialog('i/2fa/key-done', { - password: password.result, + password: auth.result.password, + token: auth.result.token, name: name.result, credential: credential.toJSON(), }); diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index 1a70c3dbfb..82b7f0ae4c 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -67,18 +67,16 @@ const onChangeReceiveAnnouncementEmail = (v) => { }); }; -const saveEmailAddress = () => { - os.inputText({ - title: i18n.ts.password, - type: 'password', - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.apiWithDialog('i/update-email', { - password: password, - email: emailAddress.value, - }); +async function saveEmailAddress() { + const auth = await os.authenticateDialog(); + if (auth.canceled) return; + + os.apiWithDialog('i/update-email', { + password: auth.result.password, + token: auth.result.token, + email: emailAddress.value, }); -}; +} const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention')); const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply')); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index c3278c22f3..e2fc021099 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -113,14 +113,12 @@ async function deleteAccount() { if (canceled) return; } - const { canceled, result: password } = await os.inputText({ - title: i18n.ts.password, - type: 'password', - }); - if (canceled) return; + const auth = await os.authenticateDialog(); + if (auth.canceled) return; await os.apiWithDialog('i/delete-account', { - password: password, + password: auth.result.password, + token: auth.result.token, }); await os.alert({ diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 7b04ab974b..eacd34778d 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -55,13 +55,6 @@ const pagination = { }; async function change() { - const { canceled: canceled1, result: currentPassword } = await os.inputText({ - title: i18n.ts.currentPassword, - type: 'password', - autocomplete: 'current-password', - }); - if (canceled1) return; - const { canceled: canceled2, result: newPassword } = await os.inputText({ title: i18n.ts.newPassword, type: 'password', @@ -84,21 +77,23 @@ async function change() { return; } + const auth = await os.authenticateDialog(); + if (auth.canceled) return; + os.apiWithDialog('i/change-password', { - currentPassword, + currentPassword: auth.result.password, + token: auth.result.token, newPassword, }); } -function regenerateToken() { - os.inputText({ - title: i18n.ts.password, - type: 'password', - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/regenerate-token', { - password: password, - }); +async function regenerateToken() { + const auth = await os.authenticateDialog(); + if (auth.canceled) return; + + os.api('i/regenerate-token', { + password: auth.result.password, + token: auth.result.token, }); } |