Add recovery codes

This commit is contained in:
KernelDeimos 2024-05-01 23:38:22 -04:00
parent 455d3946d6
commit 3bf7737790
6 changed files with 105 additions and 16 deletions

View File

@ -26,13 +26,24 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
actions.setup = async () => {
const svc_otp = x.get('services').get('otp');
// generate secret
const result = svc_otp.create_secret();
// generate recovery codes
result.codes = [];
for ( let i = 0; i < 10; i++ ) {
result.codes.push(svc_otp.create_recovery_code());
}
// update user
await db.write(
`UPDATE user SET otp_secret = ? WHERE uuid = ?`,
[result.secret, user.uuid]
`UPDATE user SET otp_secret = ?, otp_recovery_codes = ? WHERE uuid = ?`,
[result.secret, result.codes.join(','), user.uuid]
);
// update cached user
req.user.otp_secret = result.secret;
req.user.otp_recovery_codes = result.codes.join(',');
return result;
};
@ -48,7 +59,7 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
actions.disable = async () => {
await db.write(
`UPDATE user SET otp_enabled = 0 WHERE uuid = ?`,
`UPDATE user SET otp_enabled = 0, otp_recovery_codes = '' WHERE uuid = ?`,
[user.uuid]
);
return { success: true };

View File

@ -26,6 +26,16 @@ class OTPService extends BaseService {
};
}
create_recovery_code () {
const require = this.require;
const crypto = require('crypto');
const { encode } = require('hi-base32');
const buffer = crypto.randomBytes(6);
const code = encode(buffer).replace(/=/g, "").substring(0, 8);
return code;
}
verify (secret, code) {
const require = this.require;
const otpauth = require('otpauth');

View File

@ -64,10 +64,10 @@ export default {
i18n('confirm_2fa_setup'),
i18n('confirm_2fa_recovery'),
],
recovery_codes: data.codes,
has_confirm_and_cancel: true,
});
console.log('confirmation?', confirmation);
if ( ! confirmation ) return;
await fetch(`${api_origin}/auth/configure-2fa/enable`, {

View File

@ -20,6 +20,8 @@
import TeePromise from '../util/TeePromise.js';
import UIWindow from './UIWindow.js'
let checkbox_id_ = 0;
async function UIWindowQR(options){
const confirmations = options.confirmations || [];
@ -36,24 +38,45 @@ async function UIWindowQR(options){
}</h1>`;
h += `</div>`;
if ( options.recovery_codes ) {
h += `<div class="recovery-codes">`;
h += `<h2 style="text-align: center; font-size: 16px; padding: 10px; font-weight: 400; margin: -10px 10px 20px 10px; -webkit-font-smoothing: antialiased; color: #5f626d;">${
i18n('recovery_codes')
}</h2>`;
h += `<div class="recovery-codes-list">`;
for ( let i=0 ; i < options.recovery_codes.length ; i++ ) {
h += `<div class="recovery-code">${
html_encode(options.recovery_codes[i])
}</div>`;
}
h += `</div>`;
h += `</div>`;
}
for ( let i=0 ; i < confirmations.length ; i++ ) {
const confirmation = confirmations[i];
// checkbox
h += `<div class="qr-code-checkbox">`;
h += `<input type="checkbox" name="confirmation_${i}">`;
h += `<label for="confirmation_${i}">${confirmation}</label>`;
h += `<input type="checkbox" id="checkbox_${++checkbox_id_}" name="confirmation_${i}">`;
h += `<label for="checkbox_${checkbox_id_}">${confirmation}</label>`;
h += `</div>`;
}
// h += `<button class="code-confirm-btn" style="margin: 20px auto; display: block; width: 100%; padding: 10px; font-size: 16px; font-weight: 400; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer;">${
// i18n('confirm')
// }</button>`;
if ( options.has_confirm_and_cancel ) {
h += `<button type="submit" class="button button-block button-primary code-confirm-btn" style="margin-top:10px;" disabled>${
i18n('confirm')
}</button>`;
h += `<button type="submit" class="button button-block button-secondary code-cancel-btn" style="margin-top:10px;">${
i18n('cancel')
}</button>`;
} else {
h += `<button type="submit" class="button button-block button-primary code-confirm-btn" style="margin-top:10px;">${
i18n('done')
}</button>`;
}
const el_window = await UIWindow({
title: 'Instant Login!',

View File

@ -2606,11 +2606,55 @@ label {
justify-content: center;
flex-direction: column;
align-items: center;
height: 520px;
}
.otp-qr-code img {
width: 355px;
margin-bottom: 20px;
}
.recovery-codes {
border: 1px solid #ccc;
padding: 20px;
margin: 20px auto;
width: 90%;
max-width: 600px;
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.recovery-codes h2 {
text-align: center;
font-size: 18px;
color: #333;
margin-bottom: 15px;
}
.recovery-codes-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px; /* Adds space between grid items */
padding: 0;
}
.recovery-code {
background-color: #fff;
border: 1px solid #ddd;
padding: 10px;
text-align: center;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
letter-spacing: 1px;
}
.qr-code-checkbox {
display: flex;
gap: 10px;
align-items: center;
}
.qr-code-checkbox input[type=checkbox] {
margin: 0;
}
.perm-title {

View File

@ -47,7 +47,7 @@ const en = {
color: 'Color',
hue: 'Hue',
confirm_2fa_setup: 'I have added the code to my authenticator app',
confirm_2fa_recovery: 'I have saved my recovery codes',
confirm_2fa_recovery: 'I have saved my recovery codes in a secure location',
confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.',
confirm_code_generic_incorrect: "Incorrect Code.",
confirm_code_generic_title: "Enter Confirmation Code",
@ -212,6 +212,7 @@ const en = {
save_session: 'Save session',
save_session_c2a: 'Create an account to save your current session and avoid losing your work.',
scan_qr_c2a: 'Scan the code below to log into this session from other devices',
scan_qr_2fa: 'Scan the QR code with your authenticator app',
scan_qr_generic: 'Scan this QR code using your phone or another device',
seconds: 'seconds',
security: "Security",