diff --git a/packages/backend/src/routers/auth/configure-2fa.js b/packages/backend/src/routers/auth/configure-2fa.js
index b355086ea..231aeb2b3 100644
--- a/packages/backend/src/routers/auth/configure-2fa.js
+++ b/packages/backend/src/routers/auth/configure-2fa.js
@@ -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 };
diff --git a/packages/backend/src/services/auth/OTPService.js b/packages/backend/src/services/auth/OTPService.js
index 7c61e5ee8..37d6b7a87 100644
--- a/packages/backend/src/services/auth/OTPService.js
+++ b/packages/backend/src/services/auth/OTPService.js
@@ -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');
diff --git a/src/UI/Settings/UITabSecurity.js b/src/UI/Settings/UITabSecurity.js
index 586280beb..17ba0e113 100644
--- a/src/UI/Settings/UITabSecurity.js
+++ b/src/UI/Settings/UITabSecurity.js
@@ -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`, {
diff --git a/src/UI/UIWindowQR.js b/src/UI/UIWindowQR.js
index 281e12f1c..04eb1f888 100644
--- a/src/UI/UIWindowQR.js
+++ b/src/UI/UIWindowQR.js
@@ -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){
}`;
h += ``;
+ if ( options.recovery_codes ) {
+ h += `
`;
+ h += `
${
+ i18n('recovery_codes')
+ }
`;
+ h += `
`;
+ for ( let i=0 ; i < options.recovery_codes.length ; i++ ) {
+ h += `
${
+ html_encode(options.recovery_codes[i])
+ }
`;
+ }
+ h += `
`;
+ h += `
`;
+ }
+
for ( let i=0 ; i < confirmations.length ; i++ ) {
const confirmation = confirmations[i];
// checkbox
h += ``;
- h += ``;
- h += ``;
+ h += ``;
+ h += ``;
h += `
`;
}
// h += ``;
- h += ``;
- h += ``;
+ if ( options.has_confirm_and_cancel ) {
+ h += ``;
+ h += ``;
+ } else {
+ h += ``;
+ }
const el_window = await UIWindow({
title: 'Instant Login!',
diff --git a/src/css/style.css b/src/css/style.css
index cd0d35a34..24a7572c7 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -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 {
diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js
index 753b905e2..0c5d37acb 100644
--- a/src/i18n/translations/en.js
+++ b/src/i18n/translations/en.js
@@ -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",