OTP security hardened
This commit is contained in:
@@ -152,6 +152,11 @@ function renderUsers(users) {
|
||||
privilegeLabel.append(privilegeCheckbox, document.createTextNode(' Administrator'));
|
||||
row.append(label, privilegeLabel);
|
||||
if (!isCurrentUser) {
|
||||
const otpButton = document.createElement('button');
|
||||
otpButton.type = 'button';
|
||||
otpButton.textContent = 'Reset OTP';
|
||||
otpButton.addEventListener('click', () => resetUserOtp(user, otpButton));
|
||||
row.append(otpButton);
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'danger-button';
|
||||
@@ -163,6 +168,27 @@ function renderUsers(users) {
|
||||
}));
|
||||
}
|
||||
|
||||
async function resetUserOtp(user, button) {
|
||||
if (!window.confirm(`Disable OTP for ${user.username}?`)) return;
|
||||
button.disabled = true;
|
||||
const status = document.querySelector('#user-status');
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${encodeURIComponent(user.id)}/otp/reset`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await responseError(response, `Request failed (${response.status})`));
|
||||
}
|
||||
status.textContent = `OTP disabled for ${user.username}.`;
|
||||
status.style.color = '#94e2d5';
|
||||
} catch (error) {
|
||||
status.textContent = `Could not reset OTP for ${user.username}: ${error.message}`;
|
||||
status.style.color = '#f38ba8';
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const response = await fetch('/api/admin/users', {headers: authHeaders()});
|
||||
if (!response.ok) throw new Error('Could not load users');
|
||||
|
||||
@@ -12,11 +12,13 @@ const mastodonConnectButton = document.querySelector('#mastodon-connect');
|
||||
const otpSetupButton = document.querySelector('#otp-setup');
|
||||
const otpEnableButton = document.querySelector('#otp-enable');
|
||||
const otpDisableButton = document.querySelector('#otp-disable');
|
||||
const otpRecoverButton = document.querySelector('#otp-recover');
|
||||
const otpProvisioning = document.querySelector('#otp-provisioning');
|
||||
const otpDisabled = document.querySelector('#otp-disabled');
|
||||
const otpEnabled = document.querySelector('#otp-enabled');
|
||||
const otpSecret = document.querySelector('#otp-secret');
|
||||
const otpUri = document.querySelector('#otp-uri');
|
||||
const otpRecoveryCodes = document.querySelector('#otp-recovery-codes');
|
||||
const otpStatus = document.querySelector('#otp-status');
|
||||
const emailAddressList = document.querySelector('#email-address-list');
|
||||
const additionalEmailForm = document.querySelector('#additional-email-form');
|
||||
@@ -131,6 +133,7 @@ otpSetupButton.addEventListener('click', async () => {
|
||||
}
|
||||
otpSecret.textContent = result.secret;
|
||||
otpUri.href = result.otpauth_url;
|
||||
otpRecoveryCodes.textContent = result.recovery_codes.join('\n');
|
||||
otpProvisioning.classList.remove('hidden');
|
||||
setOtpStatus('Enter a code from your authenticator app to confirm setup.');
|
||||
});
|
||||
@@ -153,8 +156,9 @@ otpEnableButton.addEventListener('click', async () => {
|
||||
|
||||
otpDisableButton.addEventListener('click', async () => {
|
||||
const code = document.querySelector('#otp-disable-code').value.trim();
|
||||
const currentPassword = document.querySelector('#otp-current-password').value;
|
||||
const response = await fetch('/api/user/otp', {
|
||||
method: 'POST', headers: authHeaders(true), body: JSON.stringify({action: 'disable', code}),
|
||||
method: 'POST', headers: authHeaders(true), body: JSON.stringify({action: 'disable', code, current_password: currentPassword}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
@@ -167,6 +171,24 @@ otpDisableButton.addEventListener('click', async () => {
|
||||
setOtpStatus('One-time password disabled.');
|
||||
});
|
||||
|
||||
otpRecoverButton.addEventListener('click', async () => {
|
||||
const currentPassword = document.querySelector('#otp-current-password').value;
|
||||
const recoveryCode = document.querySelector('#otp-recovery-code').value.trim();
|
||||
const response = await fetch('/api/user/otp/recover', {
|
||||
method: 'POST', headers: authHeaders(true), body: JSON.stringify({current_password: currentPassword, recovery_code: recoveryCode}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
setOtpStatus(result.detail || 'Could not recover one-time password access.', true);
|
||||
return;
|
||||
}
|
||||
otpDisabled.classList.remove('hidden');
|
||||
otpEnabled.classList.add('hidden');
|
||||
document.querySelector('#otp-current-password').value = '';
|
||||
document.querySelector('#otp-recovery-code').value = '';
|
||||
setOtpStatus('One-time password access recovered.');
|
||||
});
|
||||
|
||||
async function loadProfile() {
|
||||
const response = await fetch('/api/user/me', {headers: authHeaders()});
|
||||
if (!response.ok) throw new Error('Could not load profile');
|
||||
|
||||
@@ -108,13 +108,19 @@
|
||||
<label>Verification code <input id="otp-setup-code" inputmode="numeric"
|
||||
autocomplete="one-time-code" /></label>
|
||||
<button id="otp-enable" type="button">Enable one-time password</button>
|
||||
<p>Save these recovery codes in a secure place. They are shown only once:</p>
|
||||
<code id="otp-recovery-codes"></code>
|
||||
</div>
|
||||
</div>
|
||||
<div id="otp-enabled" class="hidden">
|
||||
<p>One-time password is enabled.</p>
|
||||
<label>Current password <input id="otp-current-password" type="password" autocomplete="current-password" /></label>
|
||||
<label>Verification code <input id="otp-disable-code" inputmode="numeric"
|
||||
autocomplete="one-time-code" /></label>
|
||||
<button id="otp-disable" type="button">Disable one-time password</button>
|
||||
<p>Lost access to your authenticator? Use a saved recovery code.</p>
|
||||
<label>Recovery code <input id="otp-recovery-code" type="text" autocomplete="one-time-code" /></label>
|
||||
<button id="otp-recover" type="button">Recover and disable one-time password</button>
|
||||
</div>
|
||||
<p id="otp-status" class="status" role="status"></p>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user