Skip to content

Commit

Permalink
Require password entry to disable 2FA
Browse files Browse the repository at this point in the history
  • Loading branch information
KernelDeimos committed May 13, 2024
1 parent 23215bd commit c2f1694
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 42 deletions.
16 changes: 0 additions & 16 deletions packages/backend/src/routers/auth/configure-2fa.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,6 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
return {};
};

actions.disable = async () => {
await db.write(
`UPDATE user SET otp_enabled = 0, otp_recovery_codes = '' WHERE uuid = ?`,
[user.uuid]
);
// update cached user
req.user.otp_enabled = 0;

const svc_email = req.services.get('email');
await svc_email.send_email({ email: user.email }, 'disabled_2fa', {
username: user.username,
});

return { success: true };
};

if ( ! actions[action] ) {
throw APIError.create('invalid_action', null, { action });
}
Expand Down
22 changes: 22 additions & 0 deletions packages/backend/src/routers/user-protected/disable-2fa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { DB_WRITE } = require("../../services/database/consts");

module.exports = {
route: '/disable-2fa',
methods: ['POST'],
handler: async (req, res, next) => {
const db = req.services.get('database').get(DB_WRITE, '2fa.disable');
await db.write(
`UPDATE user SET otp_enabled = 0, otp_recovery_codes = NULL, otp_secret = NULL WHERE uuid = ?`,
[req.user.uuid]
);
// update cached user
req.user.otp_enabled = 0;

const svc_email = req.services.get('email');
await svc_email.send_email({ email: req.user.email }, 'disabled_2fa', {
username: req.user.username,
});

res.send({ success: true });
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class EdgeRateLimitService extends BaseService {
limit: 10,
window: HOUR,
},
['/user-protected/disable-2fa']: {
limit: 10,
window: HOUR,
},
['login-otp']: {
limit: 15,
window: 30 * MINUTE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class UserProtectedEndpointsService extends BaseService {
Endpoint(
require('../../routers/user-protected/change-email.js'),
).attach(router);

Endpoint(
require('../../routers/user-protected/disable-2fa.js'),
).attach(router);
}
}

Expand Down
136 changes: 136 additions & 0 deletions src/UI/Components/PasswordEntry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Component, defineComponent } from "../../util/Component.js";

export default class PasswordEntry extends Component {
static PROPERTIES = {
spec: {},
value: {},
error: {},
on_submit: {},
show_password: {},
}

static CSS = /*css*/`
fieldset {
display: flex;
flex-direction: column;
}
input {
flex-grow: 1;
}
/* TODO: I'd rather not duplicate this */
.error {
display: none;
color: red;
border: 1px solid red;
border-radius: 4px;
padding: 9px;
margin-bottom: 15px;
text-align: center;
font-size: 13px;
}
.error-message {
display: none;
color: rgb(215 2 2);
font-size: 14px;
margin-top: 10px;
margin-bottom: 10px;
padding: 10px;
border-radius: 4px;
border: 1px solid rgb(215 2 2);
text-align: center;
}
.password-and-toggle {
display: flex;
align-items: center;
gap: 10px;
}
.password-and-toggle input {
flex-grow: 1;
}
/* TODO: DRY: This is from style.css */
input[type=text], input[type=password], input[type=email], select {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
outline: none;
-webkit-font-smoothing: antialiased;
color: #393f46;
font-size: 14px;
}
/* to prevent auto-zoom on input focus in mobile */
.device-phone input[type=text], .device-phone input[type=password], .device-phone input[type=email], .device-phone select {
font-size: 17px;
}
input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, select:focus {
border: 2px solid #01a0fd;
padding: 7px;
}
`;

create_template ({ template }) {
$(template).html(/*html*/`
<form>
<div class="error"></div>
<div class="password-and-toggle">
<input type="password" class="value-input" id="password" placeholder="${i18n('password')}" required>
<img
id="toggle-show-password"
src="${this.get('show_password')
? window.icons["eye-closed.svg"]
: window.icons["eye-open.svg"]}"
width="20"
height="20">
</div>
</form>
`);
}

on_focus () {
$(this.dom_).find('input').focus();
}

on_ready ({ listen }) {
listen('error', (error) => {
if ( ! error ) return $(this.dom_).find('.error').hide();
$(this.dom_).find('.error').text(error).show();
});

listen('value', (value) => {
// clear input
if ( value === undefined ) {
$(this.dom_).find('input').val('');
}
});

const input = $(this.dom_).find('input');
input.on('input', () => {
this.set('value', input.val());
});

const on_submit = this.get('on_submit');
if ( on_submit ) {
$(this.dom_).find('input').on('keyup', (e) => {
if ( e.key === 'Enter' ) {
on_submit();
}
});
}

$(this.dom_).find("#toggle-show-password").on("click", () => {
this.set('show_password', !this.get('show_password'));
const show_password = this.get('show_password');
// hide/show password and update icon
$(this.dom_).find("input").attr("type", show_password ? "text" : "password");
$(this.dom_).find("#toggle-show-password").attr("src", show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"])
});
}
}

defineComponent('c-password-entry', PasswordEntry);
111 changes: 85 additions & 26 deletions src/UI/Settings/UITabSecurity.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import TeePromise from "../../util/TeePromise.js";
import Button from "../Components/Button.js";
import Flexer from "../Components/Flexer.js";
import JustHTML from "../Components/JustHTML.js";
import PasswordEntry from "../Components/PasswordEntry.js";
import UIAlert from "../UIAlert.js";
import UIComponentWindow from "../UIComponentWindow.js";
import UIWindow2FASetup from "../UIWindow2FASetup.js";
import UIWindowQR from "../UIWindowQR.js";

Expand Down Expand Up @@ -64,35 +70,88 @@ export default {
});

$el_window.find('.disable-2fa').on('click', async function (e) {
const confirmation = i18n('disable_2fa_confirm');
const alert_resp = await UIAlert({
message: confirmation,
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
},
buttons:[
{
label: i18n('yes'),
value: true,
type: 'primary',
},
{
label: i18n('cancel'),
value: false,
let win, password_entry;
const password_confirm_promise = new TeePromise();
const try_password = async () => {
const value = password_entry.get('value');
const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
password: value,
}),
});
if ( resp.status !== 200 ) {
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
let message; try {
message = (await resp.json()).message;
} catch (e) {}
message = message || i18n('error_unknown_cause');
password_entry.set('error', message);
return;
}
password_confirm_promise.resolve(true);
$(win).close();
}
const password_confirm = new Flexer({
children: [
new JustHTML({
html: /*html*/`
<h3 style="text-align:center; font-weight: 500; font-size: 20px;">${
i18n('disable_2fa_confirm')
}</h3>
<p style="text-align:center; padding: 0 20px;">${
i18n('disable_2fa_instructions')
}</p>
`
}),
new Flexer({
gap: '5pt',
children: [
new PasswordEntry({
_ref: me => password_entry = me,
on_submit: async () => {
try_password();
}
}),
new Button({
label: i18n('disable_2fa'),
on_click: async () => {
try_password();
}
}),
new Button({
label: i18n('cancel'),
style: 'secondary',
on_click: async () => {
password_confirm_promise.resolve(false);
$(win).close();
}
})
]
}),
]
})
if ( ! alert_resp ) return;
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/disable`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
});
win = await UIComponentWindow({
component: password_confirm,
width: 500,
backdrop: true,
is_resizable: false,
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
padding: '20px',
},
body: JSON.stringify({}),
});
password_entry.focus();

const ok = await password_confirm_promise;
if ( ! ok ) return;

$el_window.find('.enable-2fa').show();
$el_window.find('.disable-2fa').hide();
Expand All @@ -101,4 +160,4 @@ export default {
$el_window.find('.settings-card-security').addClass('settings-card-warning');
});
}
}
}
1 change: 1 addition & 0 deletions src/i18n/translations/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const en = {
dir_published_as_website: `%strong% has been published to:`,
disable_2fa: 'Disable 2FA',
disable_2fa_confirm: "Are you sure you want to disable 2FA?",
disable_2fa_instructions: "Enter your password to disable 2FA.",
disassociate_dir: "Disassociate Directory",
download: 'Download',
download_file: 'Download File',
Expand Down

0 comments on commit c2f1694

Please sign in to comment.