Skip to content

Commit

Permalink
feat: Refine 2fa (#11766)
Browse files Browse the repository at this point in the history
* wip

* Update 2fa.qrdialog.vue

* Update 2fa.vue

* Update CHANGELOG.md

* tweak

* ✌️
  • Loading branch information
syuilo authored Aug 28, 2023
1 parent 39d9172 commit 257c4fc
Show file tree
Hide file tree
Showing 28 changed files with 267 additions and 99 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- お知らせのバナー表示やダイアログ表示が可能に
- お知らせのアイコンを設定可能に
- チャンネルをセンシティブ指定できるようになりました
- 二要素認証のバックアップコードが生成されるようになりました

### Client
- プロフィールにその人が作ったPlayの一覧出せるように
Expand Down
9 changes: 8 additions & 1 deletion locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ export interface Locale {
"administrator": string;
"token": string;
"2fa": string;
"setupOf2fa": string;
"totp": string;
"totpDescription": string;
"moderator": string;
Expand Down Expand Up @@ -1811,9 +1812,10 @@ export interface Locale {
"step1": string;
"step2": string;
"step2Click": string;
"step2Url": string;
"step2Uri": string;
"step3Title": string;
"step3": string;
"setupCompleted": string;
"step4": string;
"securityKeyNotSupported": string;
"registerTOTPBeforeKey": string;
Expand All @@ -1829,6 +1831,11 @@ export interface Locale {
"renewTOTPConfirm": string;
"renewTOTPOk": string;
"renewTOTPCancel": string;
"checkBackupCodesBeforeCloseThisWizard": string;
"backupCodes": string;
"backupCodesDescription": string;
"backupCodeUsedWarning": string;
"backupCodesExhaustedWarning": string;
};
"_permissions": {
"read:account": string;
Expand Down
15 changes: 11 additions & 4 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ aboutMisskey: "Misskeyについて"
administrator: "管理者"
token: "確認コード"
2fa: "二要素認証"
setupOf2fa: "二要素認証のセットアップ"
totp: "認証アプリ"
totpDescription: "認証アプリを使ってワンタイムパスワードを入力"
moderator: "モデレーター"
Expand Down Expand Up @@ -1729,10 +1730,11 @@ _2fa:
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
step2: "次に、表示されているQRコードをアプリでスキャンします。"
step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。"
step2Url: "デスクトップアプリでは次のURIを入力します:"
step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します"
step3Title: "確認コードを入力"
step3: "アプリに表示されている確認コード(トークン)を入力して完了です。"
step4: "これからログインするときも、同じように確認コードを入力します。"
step3: "アプリに表示されている確認コード(トークン)を入力します。"
setupCompleted: "設定が完了しました"
step4: "これからログインするときも、同じようにコードを入力します。"
securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。"
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。"
Expand All @@ -1744,9 +1746,14 @@ _2fa:
removeKeyConfirm: "{name}を削除しますか?"
whyTOTPOnlyRenew: "セキュリティキーが登録されている場合、認証アプリの設定は解除できません。"
renewTOTP: "認証アプリを再設定"
renewTOTPConfirm: "今までの認証アプリの確認コードは使用できなくなります"
renewTOTPConfirm: "今までの認証アプリの確認コードおよびバックアップコードは使用できなくなります"
renewTOTPOk: "再設定する"
renewTOTPCancel: "やめておく"
checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、以下のバックアップコードを確認してください。"
backupCodes: "バックアップコード"
backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。"
backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。"
backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。"

_permissions:
"read:account": "アカウントの情報を見る"
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/migration/1690569881926-user-2fa-backup-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class User2faBackupCodes1690569881926 {
name = 'User2faBackupCodes1690569881926'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "twoFactorBackupSecret" character varying array`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twoFactorBackupSecret"`);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ export class UserEntityService implements OnModuleInit {
preventAiLearning: profile!.preventAiLearning,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none',
hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({
where: { userId: user.id, isSpecified: true },
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/entities/UserProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export class MiUserProfile {
})
public twoFactorSecret: string | null;

@Column('varchar', {
nullable: true, array: true,
})
public twoFactorBackupSecret: string[] | null;

@Column('boolean', {
default: false,
})
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
twoFactorBackupCodesStock: {
type: 'string',
enum: ['full', 'partial', 'none'],
nullable: false, optional: false,
},
hideOnlineStatus: {
type: 'boolean',
nullable: false, optional: false,
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/server/api/SigninApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export class SigninApiService {
});
}

if (profile.twoFactorBackupSecret?.includes(token)) {
await this.userProfilesRepository.update({ userId: profile.userId }, {
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token),
});
return this.signinService.signin(request, reply, user);
}

const delta = OTPAuth.TOTP.validate({
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
digits: 6,
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/server/api/endpoints/i/2fa/done.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('not verified');
}

const backupCodes = Array.from({ length: 5 }, () => new OTPAuth.Secret().base32);

await this.userProfilesRepository.update(me.id, {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorBackupSecret: backupCodes,
twoFactorEnabled: true,
});

Expand All @@ -64,6 +67,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
detail: true,
includeSecrets: true,
}));

return {
backupCodes: backupCodes,
};
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-

await this.userProfilesRepository.update(me.id, {
twoFactorSecret: null,
twoFactorBackupSecret: null,
twoFactorEnabled: false,
usePasswordLessLogin: false,
});
Expand Down
12 changes: 6 additions & 6 deletions packages/backend/test/e2e/2fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);

const usersShowResponse = await api('/users/show', {
username,
Expand All @@ -216,7 +216,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);

const registerKeyResponse = await api('/i/2fa/register-key', {
password,
Expand Down Expand Up @@ -272,7 +272,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);

const registerKeyResponse = await api('/i/2fa/register-key', {
password,
Expand Down Expand Up @@ -329,7 +329,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);

const registerKeyResponse = await api('/i/2fa/register-key', {
password,
Expand Down Expand Up @@ -371,7 +371,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);

const registerKeyResponse = await api('/i/2fa/register-key', {
password,
Expand Down Expand Up @@ -423,7 +423,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);

const usersShowResponse = await api('/users/show', {
username,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/test/e2e/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ describe('ユーザー', () => {
preventAiLearning: user.preventAiLearning,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
twoFactorBackupCodesStock: user.twoFactorBackupCodesStock,
hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes,
hasUnreadMentions: user.hasUnreadMentions,
Expand Down Expand Up @@ -398,6 +399,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.preventAiLearning, true);
assert.strictEqual(response.isExplorable, true);
assert.strictEqual(response.isDeleted, false);
assert.strictEqual(response.twoFactorBackupCodesStock, 'none');
assert.strictEqual(response.hideOnlineStatus, false);
assert.strictEqual(response.hasUnreadSpecifiedNotes, false);
assert.strictEqual(response.hasUnreadMentions, false);
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/changes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/.storybook/fakes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import type { entities } from 'misskey-js'

export function abuseUserReport() {
Expand Down Expand Up @@ -110,6 +115,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
publicReactions: false,
securityKeys: false,
twoFactorEnabled: false,
twoFactorBackupCodesStock: 'none',
updatedAt: null,
uri: null,
url: null,
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/generate.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { existsSync, readFileSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { basename, dirname } from 'node:path/posix';
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { StorybookConfig } from '@storybook/vue3-vite';
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { addons } from '@storybook/manager-api';
import { create } from '@storybook/theming/create';

Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { type SharedOptions, rest } from 'msw';

export const onUnhandledRequest = ((req, print) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/preload-locale.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { writeFile } from 'node:fs/promises';
import locales from '../../../locales/index.js';

Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/preload-theme.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { readFile, writeFile } from 'node:fs/promises';
import JSON5 from 'json5';

Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { addons } from '@storybook/addons';
import { FORCE_REMOUNT } from '@storybook/core-events';
import { type Preview, setup } from '@storybook/vue3';
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkSignin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
</MkInput>
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="one-time-code" :spellcheck="false" required>
<MkInput v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false" required>
<template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="ti ti-123"></i></template>
</MkInput>
Expand Down
10 changes: 9 additions & 1 deletion packages/frontend/src/pages/scratchpad.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<div :class="$style.root">
<div :class="$style.editor" class="_panel">
<PrismEditor v-model="code" class="_code code" :highlight="highlighter" :lineNumbers="false"/>
<PrismEditor v-model="code" class="_monospace" :class="$style.code" :highlight="highlighter" :lineNumbers="false"/>
<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
</div>

Expand Down Expand Up @@ -175,6 +175,14 @@ definePageMetadata({
position: relative;
}

.code {
background: #2d2d2d;
color: #ccc;
font-size: 14px;
line-height: 1.5;
padding: 5px;
}

.ui {
padding: 32px;
}
Expand Down
Loading

0 comments on commit 257c4fc

Please sign in to comment.