diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-08-28 18:25:31 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-08-28 18:25:31 +0900 |
| commit | 257c4fccf1193f111686f039e06cc4d00b9dce37 (patch) | |
| tree | b502d371495bc5a6c18349eb9fd9089cee4f4fa0 /packages/backend | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-257c4fccf1193f111686f039e06cc4d00b9dce37.tar.gz misskey-257c4fccf1193f111686f039e06cc4d00b9dce37.tar.bz2 misskey-257c4fccf1193f111686f039e06cc4d00b9dce37.zip | |
feat: Refine 2fa (#11766)
* wip
* Update 2fa.qrdialog.vue
* Update 2fa.vue
* Update CHANGELOG.md
* tweak
* :v:
Diffstat (limited to 'packages/backend')
9 files changed, 45 insertions, 6 deletions
diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js new file mode 100644 index 0000000000..2049df8ea2 --- /dev/null +++ b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js @@ -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"`); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 236ee9f0b4..d12fd1b620 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -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 }, diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts index 54144cb429..0fd26f4d63 100644 --- a/packages/backend/src/models/entities/UserProfile.ts +++ b/packages/backend/src/models/entities/UserProfile.ts @@ -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, }) diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 0c205654eb..3314464c31 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -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, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index d68b2617e3..58a5cca4fc 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index e508a28cc0..2d1457b9b5 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -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, }); @@ -64,6 +67,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- detail: true, includeSecrets: true, })); + + return { + backupCodes: backupCodes, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index ee58fb2af4..e017e2ef53 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -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, }); diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 387249871e..0aa7427da8 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 8afbcbe322..2c396813ff 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -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, @@ -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); |