feat: Refine 2fa (#11766)
* wip
* Update 2fa.qrdialog.vue
* Update 2fa.vue
* Update CHANGELOG.md
* tweak
* ✌️
This commit is contained in:
@@ -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"`);
|
||||
}
|
||||
}
|
@@ -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 },
|
||||
|
@@ -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,
|
||||
})
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user