Browse Source

Merge 598d01b51e into 6729e83521

pull/7563/merge
Timshel 1 day ago
committed by GitHub
parent
commit
91c57d9e1e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      playwright/tests/login.spec.ts
  2. 19
      playwright/tests/setups/2fa.ts
  3. 18
      playwright/tests/setups/user.ts
  4. 12
      src/api/core/accounts.rs
  5. 4
      src/api/core/events.rs
  6. 61
      src/api/core/two_factor/authenticator.rs
  7. 126
      src/api/core/two_factor/duo.rs
  8. 104
      src/api/core/two_factor/email.rs
  9. 65
      src/api/core/two_factor/mod.rs
  10. 5
      src/api/core/two_factor/protected_actions.rs
  11. 181
      src/api/core/two_factor/webauthn.rs
  12. 100
      src/api/core/two_factor/yubikey.rs
  13. 11
      src/api/identity.rs
  14. 2
      src/api/mod.rs
  15. 3
      src/auth.rs
  16. 221
      src/auth/two_factor.rs
  17. 4
      src/db/models/two_factor.rs

37
playwright/tests/login.spec.ts

@ -3,7 +3,7 @@ import * as OTPAuth from "otpauth";
import * as utils from "../global-utils"; import * as utils from "../global-utils";
import { createAccount, logUser } from './setups/user'; import { createAccount, logUser } from './setups/user';
import { activateTOTP, disableTOTP } from './setups/2fa'; import { activateTOTP, disableTOTP, recoveryCodes } from './setups/2fa';
let users = utils.loadEnv(); let users = utils.loadEnv();
let totp; let totp;
@ -31,21 +31,42 @@ test('Authenticator 2fa', async ({ page }) => {
await utils.logout(test, page, users.user1); await utils.logout(test, page, users.user1);
await test.step('login', async () => { await logUser(test, page, users.user1, { totp });
let timestamp = Date.now(); // Needed to use the next token
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000; await disableTOTP(test, page, users.user1);
});
test('Recovery codes', async ({ context, page }) => {
await logUser(test, page, users.user1);
await activateTOTP(test, page, users.user1);
let recovery = await recoveryCodes(test, page, users.user1);
await utils.logout(test, page, users.user1);
await test.step('login', async () => {
await page.getByLabel(/Email address/).fill(users.user1.email); await page.getByLabel(/Email address/).fill(users.user1.email);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password);
await page.getByRole('button', { name: 'Log in', exact: true }).click(); await page.getByRole('button', { name: 'Log in', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page).toHaveTitle(/Vaultwarden Web/);
});
await disableTOTP(test, page, users.user1); const newPagePromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Use your recovery code' }).click();
const newPage = await newPagePromise;
const tabs = context.pages();
await tabs[1].bringToFront();
await expect(tabs[1].getByRole('heading', { name: 'Recover account two-step login' })).toBeVisible();
await tabs[1].getByRole('textbox', { name: 'Email address * (required)' }).fill(users.user1.email);
await tabs[1].getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password);
await tabs[1].getByRole('textbox', { name: 'Recovery code * (required)' }).fill(recovery);
await tabs[1].getByRole('button', { name: 'Submit' }).click();
await expect(tabs[1]).toHaveTitle(/Two-step login/);
});
}); });

19
playwright/tests/setups/2fa.ts

@ -4,6 +4,24 @@ import * as OTPAuth from "otpauth";
import * as utils from '../../global-utils'; import * as utils from '../../global-utils';
export async function recoveryCodes(test: Test, page: Page, user: { name: string, password: string }): string {
return await test.step('Recovery code', async () => {
await page.getByRole('button', { name: user.name }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('link', { name: 'Security' }).click();
await page.getByRole('link', { name: 'Two-step login' }).click();
await page.getByRole('button', { name: 'View recovery code' }).click();
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
await page.getByRole('button', { name: 'Continue' }).click();
const recovery = await page.getByRole('code').innerText();
await page.getByLabel('Close').click();
return recovery;
})
}
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP { export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
return await test.step('Activate TOTP 2FA', async () => { return await test.step('Activate TOTP 2FA', async () => {
await page.getByRole('button', { name: user.name }).click(); await page.getByRole('button', { name: user.name }).click();
@ -21,7 +39,6 @@ export async function activateTOTP(test: Test, page: Page, user: { name: string,
await page.getByLabel(/Verification code/).fill(totp.generate()); await page.getByLabel(/Verification code/).fill(totp.generate());
await page.getByRole('button', { name: 'Turn on' }).click(); await page.getByRole('button', { name: 'Turn on' }).click();
await page.getByRole('heading', { name: 'Turned on', exact: true }); await page.getByRole('heading', { name: 'Turned on', exact: true });
await page.getByLabel('Close').click();
return totp; return totp;
}) })

18
playwright/tests/setups/user.ts

@ -2,6 +2,7 @@ import { expect, type Browser, Page } from '@playwright/test';
import { type MailBuffer } from 'maildev'; import { type MailBuffer } from 'maildev';
import * as OTPAuth from "otpauth";
import * as utils from '../../global-utils'; import * as utils from '../../global-utils';
import { retrieveEmailCode } from './2fa'; import { retrieveEmailCode } from './2fa';
@ -43,6 +44,7 @@ export async function logUser(
mailBuffer ?: MailBuffer, mailBuffer ?: MailBuffer,
mail2fa?: boolean, mail2fa?: boolean,
notNewDevice?: boolean, notNewDevice?: boolean,
totp?: OTPAuth.TOTP,
} = {} } = {}
) { ) {
await test.step(`Log user ${user.email}`, async () => { await test.step(`Log user ${user.email}`, async () => {
@ -55,11 +57,23 @@ export async function logUser(
await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password);
await page.getByRole('button', { name: 'Log in', exact: true }).click(); await page.getByRole('button', { name: 'Log in', exact: true }).click();
if( options.mail2fa ){ if( options.mail2fa || options.totp ){
let code;
await test.step('2FA check', async () => { await test.step('2FA check', async () => {
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
let code = await retrieveEmailCode(test, page, options.mailBuffer);
if( options.totp ) {
const totp = options.totp;
let timestamp = Date.now(); // Needed to use the next token
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
code = totp.generate({timestamp});
} else if( options.mail2fa ){
code = await retrieveEmailCode(test, page, mailBuffer);
}
await page.getByLabel(/Verification code/).fill(code); await page.getByLabel(/Verification code/).fill(code);
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
}); });
} }

12
src/api/core/accounts.rs

@ -487,8 +487,7 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
Membership::accept_user_invitations(&user.uuid, &conn).await?; Membership::accept_user_invitations(&user.uuid, &conn).await?;
} }
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) log_user_event(EventType::UserChangedPassword, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
.await;
user.save(&conn).await?; user.save(&conn).await?;
@ -613,8 +612,7 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbCon
err!("Invalid password") err!("Invalid password")
} }
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) log_user_event(EventType::UserChangedPassword, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
.await;
let (new_master_password_hash, new_key) = let (new_master_password_hash, new_key) =
if let (Some(unlock_data), Some(authentication_data)) = (data.unlock_data, data.authentication_data) { if let (Some(unlock_data), Some(authentication_data)) = (data.unlock_data, data.authentication_data) {
@ -1620,7 +1618,7 @@ async fn post_auth_request(
nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &conn).await; nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &conn).await;
log_user_event( log_user_event(
EventType::UserRequestedDeviceApproval as i32, EventType::UserRequestedDeviceApproval,
&user.uuid, &user.uuid,
client_headers.device_type, client_headers.device_type,
&client_headers.ip.ip, &client_headers.ip.ip,
@ -1714,7 +1712,7 @@ async fn put_auth_request(
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await; nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await;
log_user_event( log_user_event(
EventType::OrganizationUserApprovedAuthRequest as i32, EventType::OrganizationUserApprovedAuthRequest,
&headers.user.uuid, &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,
@ -1725,7 +1723,7 @@ async fn put_auth_request(
// If denied, there's no reason to keep the request // If denied, there's no reason to keep the request
auth_request.delete(&conn).await?; auth_request.delete(&conn).await?;
log_user_event( log_user_event(
EventType::OrganizationUserRejectedAuthRequest as i32, EventType::OrganizationUserRejectedAuthRequest,
&headers.user.uuid, &headers.user.uuid,
headers.device.atype, headers.device.atype,
&headers.ip.ip, &headers.ip.ip,

4
src/api/core/events.rs

@ -225,11 +225,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
Ok(()) Ok(())
} }
pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &DbConn) { pub async fn log_user_event(event_type: EventType, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &DbConn) {
if !CONFIG.org_events_enabled() { if !CONFIG.org_events_enabled() {
return; return;
} }
log_user_event_impl(event_type, user_id, device_type, None, ip, conn).await; log_user_event_impl(event_type as i32, user_id, device_type, None, ip, conn).await;
} }
async fn log_user_event_impl( async fn log_user_event_impl(

61
src/api/core/two_factor/authenticator.rs

@ -3,7 +3,7 @@ use rocket::{Route, serde::json::Json};
use crate::{ use crate::{
api::{EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event, core::two_factor::generate_recover_code}, api::{EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event, core::two_factor::generate_recover_code},
auth::{ClientIp, Headers}, auth::{ClientIp, Headers, two_factor},
crypto, crypto,
db::{ db::{
DbConn, DbConn,
@ -20,27 +20,23 @@ pub fn routes() -> Vec<Route> {
#[post("/two-factor/get-authenticator", data = "<data>")] #[post("/two-factor/get-authenticator", data = "<data>")]
async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult { async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user; let user = headers.user;
data.validate(&user, false, &conn).await?; data.validate(&user, false, &conn).await?;
let type_ = TwoFactorType::Authenticator as i32; let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Authenticator, &conn).await;
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await;
let (enabled, key) = match twofactor { let (enabled, key) = match twofactor {
Some(tf) => (true, tf.data), Some(tf) => (true, tf.data),
_ => (false, crypto::encode_random_bytes::<20>(&BASE32)), _ => (false, crypto::encode_random_bytes::<20>(&BASE32)),
}; };
// Upstream seems to also return `userVerificationToken`, but doesn't seem to be used at all.
// It should help prevent TOTP disclosure if someone keeps their vault unlocked.
// Since it doesn't seem to be used, and also does not cause any issues, lets leave it out of the response.
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Controllers/TwoFactorController.cs#L94
Ok(Json(json!({ Ok(Json(json!({
"enabled": enabled, "authenticator": json!({
"key": key, "enabled": enabled,
"object": "twoFactorAuthenticator" "key": key,
}),
"userVerificationToken": two_factor::authenticator_token(user.uuid, key, enabled),
}))) })))
} }
@ -49,8 +45,7 @@ async fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers,
struct EnableAuthenticatorData { struct EnableAuthenticatorData {
key: String, key: String,
token: NumberOrString, token: NumberOrString,
master_password_hash: Option<String>, user_verification_token: String,
otp: Option<String>,
} }
#[post("/two-factor/authenticator", data = "<data>")] #[post("/two-factor/authenticator", data = "<data>")]
@ -61,12 +56,7 @@ async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: He
let mut user = headers.user; let mut user = headers.user;
PasswordOrOtpData { two_factor::validate_authenticator(&data.user_verification_token, &user.uuid, &key, false)?;
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &conn)
.await?;
// Validate key as base32 and 20 bytes length // Validate key as base32 and 20 bytes length
let decoded_key: Vec<u8> = if let Ok(decoded) = BASE32.decode(key.as_bytes()) { let decoded_key: Vec<u8> = if let Ok(decoded) = BASE32.decode(key.as_bytes()) {
@ -84,12 +74,13 @@ async fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: He
generate_recover_code(&mut user, &conn).await; generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; log_user_event(EventType::UserUpdated2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
Ok(Json(json!({ Ok(Json(json!({
"enabled": true, "authenticator": json!({
"key": key, "enabled": true,
"object": "twoFactorAuthenticator" "key": key,
}),
}))) })))
} }
@ -125,8 +116,7 @@ pub async fn validate_totp_code(
err!("Invalid TOTP secret") err!("Invalid TOTP secret")
}; };
let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator, conn).await {
{
Some(tf) => tf, Some(tf) => tf,
_ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_owned()), _ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_owned()),
}; };
@ -184,24 +174,19 @@ pub async fn validate_totp_code(
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct DisableAuthenticatorData { struct DisableAuthenticatorData {
key: String, key: String,
master_password_hash: String, user_verification_token: String,
r#type: NumberOrString,
} }
#[delete("/two-factor/authenticator", data = "<data>")] #[delete("/two-factor/authenticator", data = "<data>")]
async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult { async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, conn: DbConn) -> EmptyResult {
let user = headers.user; let user = headers.user;
let type_ = data.r#type.into_i32()?;
if !user.check_valid_password(&data.master_password_hash) { two_factor::validate_authenticator(&data.user_verification_token, &user.uuid, &data.key, true)?;
err!("Invalid password");
}
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Authenticator, &conn).await {
if twofactor.data == data.key { if twofactor.data == data.key {
twofactor.delete(&conn).await?; twofactor.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) log_user_event(EventType::UserDisabled2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
.await;
} else { } else {
err!(format!("TOTP key for user {} does not match recorded value, cannot deactivate", &user.email)); err!(format!("TOTP key for user {} does not match recorded value, cannot deactivate", &user.email));
} }
@ -211,9 +196,5 @@ async fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: He
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?; super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
} }
Ok(Json(json!({ Ok(())
"enabled": false,
"keys": type_,
"object": "twoFactorProvider"
})))
} }

126
src/api/core/two_factor/duo.rs

@ -5,10 +5,11 @@ use rocket::{Route, serde::json::Json};
use crate::{ use crate::{
CONFIG, CONFIG,
api::{ api::{
ApiResult, EmptyResult, JsonResult, PasswordOrOtpData, core::log_user_event, ApiResult, EmptyResult, JsonResult, PasswordOrOtpData,
core::two_factor::generate_recover_code, core::log_user_event,
core::two_factor::{VerificationTokenData, generate_recover_code},
}, },
auth::Headers, auth::{Headers, two_factor, two_factor::DuoData},
crypto, crypto,
db::{ db::{
DbConn, DbConn,
@ -19,55 +20,7 @@ use crate::{
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![get_duo, activate_duo, activate_duo_put,] routes![get_duo, activate_duo, activate_duo_put, disable_duo,]
}
#[derive(Serialize, Deserialize)]
struct DuoData {
host: String, // Duo API hostname
ik: String, // client id
sk: String, // client secret
}
impl DuoData {
fn global() -> Option<Self> {
match (CONFIG._enable_duo(), CONFIG.duo_host()) {
(true, Some(host)) => Some(Self {
host,
ik: CONFIG.duo_ikey().unwrap(),
sk: CONFIG.duo_skey().unwrap(),
}),
_ => None,
}
}
fn msg(s: &str) -> Self {
Self {
host: s.into(),
ik: s.into(),
sk: s.into(),
}
}
fn secret() -> Self {
Self::msg("<global_secret>")
}
fn obscure(self) -> Self {
let mut host = self.host;
let mut ik = self.ik;
let mut sk = self.sk;
let digits = 4;
let replaced = "************";
host.replace_range(digits.., replaced);
ik.replace_range(digits.., replaced);
sk.replace_range(digits.., replaced);
Self {
host,
ik,
sk,
}
}
} }
enum DuoStatus { enum DuoStatus {
@ -96,22 +49,19 @@ async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn)
data.validate(&user, false, &conn).await?; data.validate(&user, false, &conn).await?;
let data = get_user_duo_data(&user.uuid, &conn).await; let (enabled, duo) = match get_user_duo_data(&user.uuid, &conn).await {
let (enabled, data) = match data {
DuoStatus::Global(_) => (true, Some(DuoData::secret())), DuoStatus::Global(_) => (true, Some(DuoData::secret())),
DuoStatus::User(data) => (true, Some(data.obscure())), DuoStatus::User(data) => (true, Some(data.obscure())),
DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))), DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
DuoStatus::Disabled(false) => (false, None), DuoStatus::Disabled(false) => (false, None),
}; };
let json = if let Some(data) = data { let duo_json = if let Some(data) = duo.as_ref() {
json!({ json!({
"enabled": enabled, "enabled": enabled,
"host": data.host, "host": data.host,
"clientSecret": data.sk, "clientSecret": data.sk,
"clientId": data.ik, "clientId": data.ik,
"object": "twoFactorDuo"
}) })
} else { } else {
json!({ json!({
@ -119,11 +69,13 @@ async fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn)
"host": null, "host": null,
"clientSecret": null, "clientSecret": null,
"clientId": null, "clientId": null,
"object": "twoFactorDuo"
}) })
}; };
Ok(Json(json)) Ok(Json(rocket::serde::json::json!({
"duo": duo_json,
"userVerificationToken": two_factor::duo_token(user.uuid, duo, enabled),
})))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -132,8 +84,7 @@ struct EnableDuoData {
host: String, host: String,
client_secret: String, client_secret: String,
client_id: String, client_id: String,
master_password_hash: Option<String>, user_verification_token: String,
otp: Option<String>,
} }
impl From<EnableDuoData> for DuoData { impl From<EnableDuoData> for DuoData {
@ -160,12 +111,7 @@ async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, conn: DbConn)
let data: EnableDuoData = data.into_inner(); let data: EnableDuoData = data.into_inner();
let mut user = headers.user; let mut user = headers.user;
PasswordOrOtpData { two_factor::validate_duo(&data.user_verification_token, &user.uuid, None, false)?;
master_password_hash: data.master_password_hash.clone(),
otp: data.otp.clone(),
}
.validate(&user, true, &conn)
.await?;
let (data, data_str) = if check_duo_fields_custom(&data) { let (data, data_str) = if check_duo_fields_custom(&data) {
let data_req: DuoData = data.into(); let data_req: DuoData = data.into();
@ -182,14 +128,15 @@ async fn activate_duo(data: Json<EnableDuoData>, headers: Headers, conn: DbConn)
generate_recover_code(&mut user, &conn).await; generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; log_user_event(EventType::UserUpdated2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
Ok(Json(json!({ Ok(Json(json!({
"enabled": true, "duo": json!({
"host": data.host, "enabled": true,
"clientSecret": data.sk, "host": data.host,
"clientId": data.ik, "clientSecret": data.sk,
"object": "twoFactorDuo" "clientId": data.ik,
}),
}))) })))
} }
@ -198,6 +145,31 @@ async fn activate_duo_put(data: Json<EnableDuoData>, headers: Headers, conn: DbC
activate_duo(data, headers, conn).await activate_duo(data, headers, conn).await
} }
#[delete("/two-factor/duo", data = "<data>")]
async fn disable_duo(data: Json<VerificationTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
let user = headers.user;
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Duo, &conn).await {
// Apply the same transformation than in `get_duo` to check we are disabling the correct one
let duo = match to_user_duo_data(&twofactor) {
DuoStatus::Global(_) => Some(DuoData::secret()),
DuoStatus::User(data) => Some(data.obscure()),
DuoStatus::Disabled(_) => None,
};
two_factor::validate_duo(&data.user_verification_token, &user.uuid, duo.as_ref(), true)?;
twofactor.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
}
if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
}
Ok(())
}
async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult { async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
use reqwest::{Method, header}; use reqwest::{Method, header};
use std::str::FromStr; use std::str::FromStr;
@ -230,13 +202,15 @@ const DUO_PREFIX: &str = "TX";
const APP_PREFIX: &str = "APP"; const APP_PREFIX: &str = "APP";
async fn get_user_duo_data(user_id: &UserId, conn: &DbConn) -> DuoStatus { async fn get_user_duo_data(user_id: &UserId, conn: &DbConn) -> DuoStatus {
let type_ = TwoFactorType::Duo as i32;
// If the user doesn't have an entry, disabled // If the user doesn't have an entry, disabled
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await else { let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Duo, conn).await else {
return DuoStatus::Disabled(DuoData::global().is_some()); return DuoStatus::Disabled(DuoData::global().is_some());
}; };
to_user_duo_data(&twofactor)
}
fn to_user_duo_data(twofactor: &TwoFactor) -> DuoStatus {
// If the user has the required values, we use those // If the user has the required values, we use those
if let Ok(data) = serde_json::from_str(&twofactor.data) { if let Ok(data) = serde_json::from_str(&twofactor.data) {
return DuoStatus::User(data); return DuoStatus::User(data);

104
src/api/core/two_factor/email.rs

@ -5,9 +5,12 @@ use crate::{
CONFIG, CONFIG,
api::{ api::{
EmptyResult, JsonResult, PasswordOrOtpData, EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_user_event, two_factor::generate_recover_code}, core::{
log_user_event,
two_factor::{VerificationTokenData, generate_recover_code},
},
}, },
auth::{ClientHeaders, Headers}, auth::{ClientHeaders, Headers, two_factor},
crypto, crypto,
db::{ db::{
DbConn, DbConn,
@ -18,7 +21,7 @@ use crate::{
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![get_email, send_email_login, send_email, email,] routes![get_email, send_email_login, send_email, email, disable_email]
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -107,8 +110,8 @@ async fn send_email_login(data: Json<SendEmailLoginData>, client_headers: Client
/// Generate the token, save the data for later verification and send email to user /// Generate the token, save the data for later verification and send email to user
pub async fn send_token(user_id: &UserId, conn: &DbConn) -> EmptyResult { pub async fn send_token(user_id: &UserId, conn: &DbConn) -> EmptyResult {
let type_ = TwoFactorType::Email as i32; let mut twofactor =
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, type_, conn).await.map_res("Two factor not found")?; TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email, conn).await.map_res("Two factor not found")?;
let generated_token = crypto::generate_email_token(CONFIG.email_token_size()); let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
@ -131,18 +134,19 @@ async fn get_email(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn
data.validate(&user, false, &conn).await?; data.validate(&user, false, &conn).await?;
let (enabled, mfa_email) = let (enabled, mfa_email) =
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &conn).await { if let Some(x) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email, &conn).await {
Some(x) => { let twofactor_data = EmailTokenData::from_json(&x.data)?;
let twofactor_data = EmailTokenData::from_json(&x.data)?; (true, Some(twofactor_data.email))
(true, json!(twofactor_data.email)) } else {
} (false, None)
_ => (false, serde_json::value::Value::Null),
}; };
Ok(Json(json!({ Ok(Json(rocket::serde::json::json!({
"email": mfa_email, "email": rocket::serde::json::json!({
"enabled": enabled, "enabled": enabled,
"object": "twoFactorEmail" "email": mfa_email,
}),
"userVerificationToken": two_factor::email_token(user.uuid, mfa_email, enabled),
}))) })))
} }
@ -151,30 +155,22 @@ async fn get_email(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn
struct SendEmailData { struct SendEmailData {
/// Email where 2FA codes will be sent to, can be different than user email account. /// Email where 2FA codes will be sent to, can be different than user email account.
email: String, email: String,
master_password_hash: Option<String>, user_verification_token: String,
otp: Option<String>,
} }
/// Send a verification email to the specified email address to check whether it exists/belongs to user. /// Send a verification email to the specified email address to check whether it exists/belongs to user.
#[post("/two-factor/send-email", data = "<data>")] #[post("/two-factor/send-email", data = "<data>")]
async fn send_email(data: Json<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult { async fn send_email(data: Json<SendEmailData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: SendEmailData = data.into_inner(); let data: SendEmailData = data.into_inner();
let user = headers.user; let user = headers.user;
PasswordOrOtpData { two_factor::validate_email(&data.user_verification_token, &user.uuid, data.email.clone(), false)?;
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, false, &conn)
.await?;
if !CONFIG._enable_email_2fa() { if !CONFIG._enable_email_2fa() {
err!("Email 2FA is disabled") err!("Email 2FA is disabled")
} }
let type_ = TwoFactorType::Email as i32; if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email, &conn).await {
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
tf.delete(&conn).await?; tf.delete(&conn).await?;
} }
@ -182,12 +178,13 @@ async fn send_email(data: Json<SendEmailData>, headers: Headers, conn: DbConn) -
let twofactor_data = EmailTokenData::new(data.email, generated_token); let twofactor_data = EmailTokenData::new(data.email, generated_token);
// Uses EmailVerificationChallenge as type to show that it's not verified yet. // Uses EmailVerificationChallenge as type to show that it's not verified yet.
let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json()); let twofactor =
TwoFactor::new(user.uuid.clone(), TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json());
twofactor.save(&conn).await?; twofactor.save(&conn).await?;
mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?).await?; mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res("Token is empty")?).await?;
Ok(()) Ok(Json(json!({})))
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -195,8 +192,7 @@ async fn send_email(data: Json<SendEmailData>, headers: Headers, conn: DbConn) -
struct EmailData { struct EmailData {
email: String, email: String,
token: String, token: String,
master_password_hash: Option<String>, user_verification_token: String,
otp: Option<String>,
} }
/// Verify email belongs to user and can be used for 2FA email codes. /// Verify email belongs to user and can be used for 2FA email codes.
@ -205,17 +201,11 @@ async fn email(data: Json<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
let data: EmailData = data.into_inner(); let data: EmailData = data.into_inner();
let mut user = headers.user; let mut user = headers.user;
// This is the last step in the verification process, delete the otp directly afterwards two_factor::validate_email(&data.user_verification_token, &user.uuid, data.email, false)?;
PasswordOrOtpData {
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &conn)
.await?;
let type_ = TwoFactorType::EmailVerificationChallenge as i32; let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::EmailVerificationChallenge, &conn)
let mut twofactor = .await
TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await.map_res("Two factor not found")?; .map_res("Two factor not found")?;
let mut email_data = EmailTokenData::from_json(&twofactor.data)?; let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
@ -234,13 +224,28 @@ async fn email(data: Json<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
generate_recover_code(&mut user, &conn).await; generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; log_user_event(EventType::UserUpdated2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
Ok(Json(json!({ Ok(Json(json!({})))
"email": email_data.email, }
"enabled": "true",
"object": "twoFactorEmail" #[delete("/two-factor/email", data = "<data>")]
}))) async fn disable_email(data: Json<VerificationTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
let user = headers.user;
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email, &conn).await {
let twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
two_factor::validate_email(&data.user_verification_token, &user.uuid, twofactor_data.email, true)?;
twofactor.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
}
if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
}
Ok(())
} }
/// Validate the email code when used as TwoFactor token mechanism /// Validate the email code when used as TwoFactor token mechanism
@ -252,9 +257,8 @@ pub async fn validate_email_code_str(
conn: &DbConn, conn: &DbConn,
) -> EmptyResult { ) -> EmptyResult {
let mut email_data = EmailTokenData::from_json(data)?; let mut email_data = EmailTokenData::from_json(data)?;
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn) let mut twofactor =
.await TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email, conn).await.map_res("Two factor not found")?;
.map_res("Two factor not found")?;
let Some(issued_token) = &email_data.last_token else { let Some(issued_token) = &email_data.last_token else {
err!( err!(
format!("No token available! IP: {ip}"), format!("No token available! IP: {ip}"),

65
src/api/core/two_factor/mod.rs

@ -7,10 +7,7 @@ use serde_json::Value;
use crate::{ use crate::{
CONFIG, CONFIG,
api::{ api::{EmptyResult, JsonResult, PasswordOrOtpData, core::log_event},
EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_event, log_user_event},
},
auth::Headers, auth::Headers,
crypto, crypto,
db::{ db::{
@ -21,7 +18,6 @@ use crate::{
}, },
}, },
mail, mail,
util::NumberOrString,
}; };
pub mod authenticator; pub mod authenticator;
@ -69,13 +65,7 @@ pub fn is_twofactor_provider_usable(provider_type: &TwoFactorType, provider_data
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
let mut routes = routes![ let mut routes = routes![get_twofactor, get_recover, get_device_verification_settings,];
get_twofactor,
get_recover,
disable_twofactor,
disable_twofactor_put,
get_device_verification_settings,
];
routes.append(&mut authenticator::routes()); routes.append(&mut authenticator::routes());
routes.append(&mut duo::routes()); routes.append(&mut duo::routes());
@ -87,6 +77,12 @@ pub fn routes() -> Vec<Route> {
routes routes
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VerificationTokenData {
user_verification_token: String,
}
#[get("/two-factor")] #[get("/two-factor")]
async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> { async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await; let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await;
@ -126,51 +122,6 @@ async fn generate_recover_code(user: &mut User, conn: &DbConn) {
} }
} }
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DisableTwoFactorData {
master_password_hash: Option<String>,
otp: Option<String>,
r#type: NumberOrString,
}
#[post("/two-factor/disable", data = "<data>")]
async fn disable_twofactor(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: DisableTwoFactorData = data.into_inner();
let user = headers.user;
// Delete directly after a valid token has been provided
PasswordOrOtpData {
master_password_hash: data.master_password_hash,
otp: data.otp,
}
.validate(&user, true, &conn)
.await?;
let type_ = data.r#type.into_i32()?;
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {
twofactor.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await;
}
if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {
enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
}
Ok(Json(json!({
"enabled": false,
"type": type_,
"object": "twoFactorProvider"
})))
}
#[put("/two-factor/disable", data = "<data>")]
async fn disable_twofactor_put(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
disable_twofactor(data, headers, conn).await
}
pub async fn enforce_2fa_policy( pub async fn enforce_2fa_policy(
user: &User, user: &User,
act_user_id: &UserId, act_user_id: &UserId,

5
src/api/core/two_factor/protected_actions.rs

@ -72,8 +72,7 @@ async fn request_otp(headers: Headers, conn: DbConn) -> EmptyResult {
let user = headers.user; let user = headers.user;
// Only one Protected Action per user is allowed to take place, delete the previous one // Only one Protected Action per user is allowed to take place, delete the previous one
if let Some(pa) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions as i32, &conn).await if let Some(pa) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions, &conn).await {
{
let pa_data = ProtectedActionData::from_json(&pa.data)?; let pa_data = ProtectedActionData::from_json(&pa.data)?;
let elapsed = pa_data.time_since_sent().num_seconds(); let elapsed = pa_data.time_since_sent().num_seconds();
let delay = 30; let delay = 30;
@ -125,7 +124,7 @@ pub async fn validate_protected_action_otp(
delete_if_valid: bool, delete_if_valid: bool,
conn: &DbConn, conn: &DbConn,
) -> EmptyResult { ) -> EmptyResult {
let mut pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn) let mut pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions, conn)
.await .await
.map_res("Protected action token not found, try sending the code again or restart the process")?; .map_res("Protected action token not found, try sending the code again or restart the process")?;
let mut pa_data = ProtectedActionData::from_json(&pa.data)?; let mut pa_data = ProtectedActionData::from_json(&pa.data)?;

181
src/api/core/two_factor/webauthn.rs

@ -1,4 +1,4 @@
use std::{str::FromStr, sync::LazyLock, time::Duration}; use std::{collections::HashSet, str::FromStr, sync::LazyLock, time::Duration};
use rocket::{Route, serde::json::Json}; use rocket::{Route, serde::json::Json};
use serde_json::Value; use serde_json::Value;
@ -18,16 +18,18 @@ use crate::{
CONFIG, CONFIG,
api::{ api::{
EmptyResult, JsonResult, PasswordOrOtpData, EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_user_event, two_factor::generate_recover_code}, core::{
log_user_event,
two_factor::{VerificationTokenData, generate_recover_code},
},
}, },
auth::Headers, auth::{Headers, two_factor},
crypto::ct_eq, crypto::ct_eq,
db::{ db::{
DbConn, DbConn,
models::{EventType, TwoFactor, TwoFactorType, UserId}, models::{EventType, TwoFactor, TwoFactorType, UserId},
}, },
error::Error, error::Error,
util::NumberOrString,
}; };
static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| { static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
@ -45,7 +47,14 @@ static WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {
}); });
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] routes![
get_webauthn,
generate_webauthn_challenge,
activate_webauthn,
activate_webauthn_put,
delete_webauthn,
delete_webauthns
]
} }
// Some old u2f structs still needed for migrating from u2f to WebAuthn // Some old u2f structs still needed for migrating from u2f to WebAuthn
@ -119,34 +128,36 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbC
data.validate(&user, false, &conn).await?; data.validate(&user, false, &conn).await?;
let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &conn).await?; let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &conn).await?;
let keys: Vec<i32> = registrations.iter().map(|r| r.id).collect();
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect(); let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
Ok(Json(json!({ Ok(Json(json!({
"enabled": enabled, "webAuthn": json!({
"keys": registrations_json, "enabled": enabled,
"object": "twoFactorWebAuthn" "keys": registrations_json,
}),
"userVerificationToken": two_factor::webauthn_token(user.uuid, keys, enabled),
}))) })))
} }
#[post("/two-factor/get-webauthn-challenge", data = "<data>")] #[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult { async fn generate_webauthn_challenge(data: Json<VerificationTokenData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user; let user = headers.user;
data.validate(&user, false, &conn).await?; let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &conn).await?;
let keys: Vec<i32> = registrations.iter().map(|r| r.id).collect();
let registrations = get_webauthn_registrations(&user.uuid, &conn) let creds = registrations
.await?
.1
.into_iter() .into_iter()
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
.collect(); .collect();
two_factor::validate_webauthn(&data.user_verification_token, &user.uuid, &keys, enabled)?;
let (mut challenge, state) = WEBAUTHN.start_passkey_registration( let (mut challenge, state) = WEBAUTHN.start_passkey_registration(
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
&user.email, &user.email,
user.display_name(), user.display_name(),
Some(registrations), Some(creds),
)?; )?;
let mut state = serde_json::to_value(&state)?; let mut state = serde_json::to_value(&state)?;
@ -166,17 +177,19 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
let mut challenge_value = serde_json::to_value(challenge.public_key)?; let mut challenge_value = serde_json::to_value(challenge.public_key)?;
challenge_value["status"] = "ok".into(); challenge_value["status"] = "ok".into();
challenge_value["errorMessage"] = "".into(); challenge_value["errorMessage"] = "".into();
Ok(Json(challenge_value))
Ok(Json(json!({
"options": challenge_value
})))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct EnableWebauthnData { struct EnableWebauthnData {
id: NumberOrString, // 1..5 id: i32,
name: String, name: String,
device_response: RegisterPublicKeyCredentialCopy, device_response: RegisterPublicKeyCredentialCopy,
master_password_hash: Option<String>, user_verification_token: String,
otp: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -257,16 +270,14 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, con
let data: EnableWebauthnData = data.into_inner(); let data: EnableWebauthnData = data.into_inner();
let mut user = headers.user; let mut user = headers.user;
PasswordOrOtpData { let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1;
master_password_hash: data.master_password_hash, let keys: Vec<i32> = registrations.iter().map(|r| r.id).collect();
otp: data.otp, two_factor::validate_webauthn(&data.user_verification_token, &user.uuid, &keys, !keys.is_empty())?;
}
.validate(&user, true, &conn)
.await?;
// Retrieve and delete the saved challenge state // Retrieve and delete the saved challenge state
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32; let state = if let Some(tf) =
let state = if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::WebauthnRegisterChallenge, &conn).await
{
let state: PasskeyRegistration = serde_json::from_str(&tf.data)?; let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
tf.delete(&conn).await?; tf.delete(&conn).await?;
state state
@ -277,10 +288,9 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, con
// Verify the credentials with the saved state // Verify the credentials with the saved state
let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?; let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1;
// TODO: Check for repeated ID's // TODO: Check for repeated ID's
registrations.push(WebauthnRegistration { registrations.push(WebauthnRegistration {
id: data.id.into_i32()?, id: data.id,
name: data.name, name: data.name,
migrated: false, migrated: false,
@ -293,13 +303,15 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, con
.await?; .await?;
generate_recover_code(&mut user, &conn).await; generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; log_user_event(EventType::UserUpdated2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect(); let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
Ok(Json(json!({ Ok(Json(json!({
"enabled": true, "webAuthn": json!({
"keys": keys_json, "enabled": true,
"object": "twoFactorU2f" "keys": keys_json,
}),
}))) })))
} }
@ -310,66 +322,92 @@ async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers,
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct DeleteU2FData { struct DeleteWebauthnData {
id: NumberOrString, id: i32,
master_password_hash: String, user_verification_token: String,
} }
#[delete("/two-factor/webauthn", data = "<data>")] #[delete("/two-factor/webauthn", data = "<data>")]
async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult { async fn delete_webauthn(data: Json<DeleteWebauthnData>, headers: Headers, conn: DbConn) -> EmptyResult {
let id = data.id.into_i32()?; inner_delete_webauthns(&data.user_verification_token, |key| key.id != data.id, headers, &conn).await
if !headers.user.check_valid_password(&data.master_password_hash) { }
err!("Invalid password");
} #[delete("/two-factor/webauthn/all", data = "<data>")]
async fn delete_webauthns(data: Json<VerificationTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
inner_delete_webauthns(&data.user_verification_token, |_| false, headers, &conn).await
}
async fn inner_delete_webauthns(
token: &str,
retain: impl Fn(&WebauthnRegistration) -> bool,
headers: Headers,
conn: &DbConn,
) -> EmptyResult {
let user = headers.user;
let Some(mut tf) = let Some(mut tf) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Webauthn, conn).await else {
TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &conn).await
else {
err!("Webauthn data not found!") err!("Webauthn data not found!")
}; };
let mut data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?; let mut keys: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?;
let keys_id: Vec<i32> = keys.iter().map(|r| r.id).collect();
two_factor::validate_webauthn(token, &user.uuid, &keys_id, true)?;
let mut removed: HashSet<Vec<u8>> = HashSet::new();
let mut migrated = false;
keys.retain(|key| {
let retained = retain(key);
if !retained {
removed.insert(key.credential.cred_id().to_vec());
migrated = migrated || key.migrated;
}
retained
});
let Some(item_pos) = data.iter().position(|r| r.id == id) else { if removed.is_empty() {
err!("Webauthn entry not found") err!("Webauthn entry not found")
}; }
let removed_item = data.remove(item_pos); if keys.is_empty() {
tf.data = serde_json::to_string(&data)?; tf.delete(conn).await?;
tf.save(&conn).await?; log_user_event(EventType::UserDisabled2fa, &user.uuid, headers.device.atype, &headers.ip.ip, conn).await;
drop(tf); } else {
tf.data = serde_json::to_string(&keys)?;
tf.save(conn).await?;
drop(tf);
}
// If entry is migrated from u2f, delete the u2f entry as well // If entry is migrated from u2f, delete the u2f entry as well
if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn).await if migrated && let Some(mut u2f) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::U2f, conn).await {
{ let Ok(mut data) = serde_json::from_str::<Vec<U2FRegistration>>(&u2f.data) else {
let mut data: Vec<U2FRegistration> = if let Ok(d) = serde_json::from_str(&u2f.data) {
d
} else {
err!("Error parsing U2F data") err!("Error parsing U2F data")
}; };
data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id().as_slice()); data.retain(|old| !removed.contains(&old.reg.key_handle));
let new_data_str = serde_json::to_string(&data)?;
u2f.data = new_data_str; if data.is_empty() {
u2f.save(&conn).await?; u2f.delete(conn).await?;
} else {
let new_data_str = serde_json::to_string(&data)?;
u2f.data = new_data_str;
u2f.save(conn).await?;
}
} }
let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect(); if keys.is_empty() && TwoFactor::find_by_user(&user.uuid, conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, conn).await?;
}
Ok(Json(json!({ Ok(())
"enabled": true,
"keys": keys_json,
"object": "twoFactorU2f"
})))
} }
pub async fn get_webauthn_registrations( pub async fn get_webauthn_registrations(
user_id: &UserId, user_id: &UserId,
conn: &DbConn, conn: &DbConn,
) -> Result<(bool, Vec<WebauthnRegistration>), Error> { ) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
let type_ = TwoFactorType::Webauthn as i32; match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Webauthn, conn).await {
match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)), Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),
None => Ok((false, Vec::new())), // If no data, return empty list None => Ok((false, Vec::new())), // If no data, return empty list
} }
@ -416,8 +454,9 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes
} }
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &DbConn) -> EmptyResult { pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &DbConn) -> EmptyResult {
let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let mut state = if let Some(tf) =
let mut state = if let Some(tf) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await { TwoFactor::find_by_user_and_type(user_id, TwoFactorType::WebauthnLoginChallenge, conn).await
{
let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?; let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
tf.delete(conn).await?; tf.delete(conn).await?;
state state

100
src/api/core/two_factor/yubikey.rs

@ -10,9 +10,12 @@ use crate::{
CONFIG, CONFIG,
api::{ api::{
EmptyResult, JsonResult, PasswordOrOtpData, EmptyResult, JsonResult, PasswordOrOtpData,
core::{log_user_event, two_factor::generate_recover_code}, core::{
log_user_event,
two_factor::{VerificationTokenData, generate_recover_code},
},
}, },
auth::Headers, auth::{Headers, two_factor},
db::{ db::{
DbConn, DbConn,
models::{EventType, TwoFactor, TwoFactorType}, models::{EventType, TwoFactor, TwoFactorType},
@ -22,7 +25,7 @@ use crate::{
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] routes![generate_yubikey, activate_yubikey, activate_yubikey_put, delete_yubikeys,]
} }
struct HttpClientTransport { struct HttpClientTransport {
@ -60,8 +63,7 @@ struct EnableYubikeyData {
key4: Option<String>, key4: Option<String>,
key5: Option<String>, key5: Option<String>,
nfc: bool, nfc: bool,
master_password_hash: Option<String>, user_verification_token: String,
otp: Option<String>,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -125,48 +127,29 @@ async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, conn:
data.validate(&user, false, &conn).await?; data.validate(&user, false, &conn).await?;
let user_id = &user.uuid; let user_id = &user.uuid;
let yubikey_type = TwoFactorType::YubiKey as i32;
let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &conn).await; let (enabled, keys, yubikey_json) =
if let Some(r) = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::YubiKey, &conn).await {
if let Some(r) = r { let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?; let enabled = !yubikey_metadata.keys.is_empty();
let mut result = jsonify_yubikeys(yubikey_metadata.keys.clone());
let mut result = jsonify_yubikeys(yubikey_metadata.keys); result["enabled"] = Value::Bool(enabled);
result["nfc"] = Value::Bool(yubikey_metadata.nfc);
result["enabled"] = Value::Bool(true); (enabled, yubikey_metadata.keys, result)
result["nfc"] = Value::Bool(yubikey_metadata.nfc); } else {
result["object"] = Value::String("twoFactorU2f".to_owned()); (false, Vec::new(), json!({"enabled": false}))
};
Ok(Json(result)) Ok(Json(json!({
} else { "yubiKey": yubikey_json,
Ok(Json(json!({ "userVerificationToken": two_factor::yubikey_token(user.uuid, keys, enabled),
"enabled": false, })))
"object": "twoFactorU2f",
})))
}
} }
#[post("/two-factor/yubikey", data = "<data>")] #[post("/two-factor/yubikey", data = "<data>")]
async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult { async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableYubikeyData = data.into_inner(); let data: EnableYubikeyData = data.into_inner();
let mut user = headers.user;
PasswordOrOtpData {
master_password_hash: data.master_password_hash.clone(),
otp: data.otp.clone(),
}
.validate(&user, true, &conn)
.await?;
// Check if we already have some data
let mut yubikey_data =
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn).await {
Some(data) => data,
None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
};
let yubikeys = parse_yubikeys(&data); let yubikeys = parse_yubikeys(&data);
let mut user = headers.user;
if yubikeys.is_empty() { if yubikeys.is_empty() {
// Return an error to prevent saving empty keys which would cause users not being able to login anymore. // Return an error to prevent saving empty keys which would cause users not being able to login anymore.
@ -174,6 +157,17 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn:
err!("A key is required."); err!("A key is required.");
} }
// Check if we already have some data
let mut yubikey_data =
if let Some(yd) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey, &conn).await {
let ym: YubikeyMetadata = serde_json::from_str(&yd.data)?;
two_factor::validate_yubikey(&data.user_verification_token, &user.uuid, &ym.keys, !ym.keys.is_empty())?;
yd
} else {
two_factor::validate_yubikey(&data.user_verification_token, &user.uuid, &Vec::new(), false)?;
TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new())
};
// Ensure they are valid OTPs // Ensure they are valid OTPs
for yubikey in &yubikeys { for yubikey in &yubikeys {
if yubikey.is_empty() || yubikey.len() == 12 { if yubikey.is_empty() || yubikey.len() == 12 {
@ -195,15 +189,12 @@ async fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn:
generate_recover_code(&mut user, &conn).await; generate_recover_code(&mut user, &conn).await;
log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; log_user_event(EventType::UserUpdated2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
let mut result = jsonify_yubikeys(yubikey_metadata.keys); let mut result = jsonify_yubikeys(yubikey_metadata.keys);
result["enabled"] = Value::Bool(true); result["enabled"] = Value::Bool(true);
result["nfc"] = Value::Bool(yubikey_metadata.nfc); result["nfc"] = Value::Bool(yubikey_metadata.nfc);
result["object"] = Value::String("twoFactorU2f".to_owned()); Ok(Json(json!({"yubiKey": result})))
Ok(Json(result))
} }
#[put("/two-factor/yubikey", data = "<data>")] #[put("/two-factor/yubikey", data = "<data>")]
@ -211,6 +202,25 @@ async fn activate_yubikey_put(data: Json<EnableYubikeyData>, headers: Headers, c
activate_yubikey(data, headers, conn).await activate_yubikey(data, headers, conn).await
} }
#[delete("/two-factor/yubikey", data = "<data>")]
async fn delete_yubikeys(data: Json<VerificationTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
let user = headers.user;
if let Some(r) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey, &conn).await {
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
two_factor::validate_yubikey(&data.user_verification_token, &user.uuid, &yubikey_metadata.keys, true)?;
r.delete(&conn).await?;
log_user_event(EventType::UserDisabled2fa, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;
}
if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {
super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;
}
Ok(())
}
pub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult { pub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
if response.len() != 44 { if response.len() != 44 {
err!("Invalid Yubikey OTP length"); err!("Invalid Yubikey OTP length");

11
src/api/identity.rs

@ -129,7 +129,7 @@ async fn login(
match &login_result { match &login_result {
Ok(_) => { Ok(_) => {
log_user_event( log_user_event(
EventType::UserLoggedIn as i32, EventType::UserLoggedIn,
&user_id, &user_id,
client_header.device_type, client_header.device_type,
&client_header.ip.ip, &client_header.ip.ip,
@ -139,8 +139,7 @@ async fn login(
} }
Err(e) => { Err(e) => {
if let Some(ev) = e.get_event() { if let Some(ev) = e.get_event() {
log_user_event(ev.event as i32, &user_id, client_header.device_type, &client_header.ip.ip, &conn) log_user_event(ev.event, &user_id, client_header.device_type, &client_header.ip.ip, &conn).await;
.await;
} }
} }
} }
@ -907,7 +906,7 @@ async fn twofactor_auth(
TwoFactor::delete_all_by_user(&user.uuid, conn).await?; TwoFactor::delete_all_by_user(&user.uuid, conn).await?;
enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?; enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?;
log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await; log_user_event(EventType::UserRecovered2fa, &user.uuid, device.atype, &ip.ip, conn).await;
// Remove the recovery code, not needed without twofactors // Remove the recovery code, not needed without twofactors
user.totp_recover = None; user.totp_recover = None;
@ -993,7 +992,7 @@ async fn json_err_twofactor(
} }
Some(tf_type @ TwoFactorType::YubiKey) => { Some(tf_type @ TwoFactorType::YubiKey) => {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else { let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type, conn).await else {
err!("No YubiKey devices registered") err!("No YubiKey devices registered")
}; };
@ -1005,7 +1004,7 @@ async fn json_err_twofactor(
} }
Some(tf_type @ TwoFactorType::Email) => { Some(tf_type @ TwoFactorType::Email) => {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else { let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type, conn).await else {
err!("No twofactor email registered") err!("No twofactor email registered")
}; };

2
src/api/mod.rs

@ -46,7 +46,7 @@ pub type JsonResult = ApiResult<Json<Value>>;
pub type EmptyResult = ApiResult<()>; pub type EmptyResult = ApiResult<()>;
// Common structs representing JSON data received // Common structs representing JSON data received
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct PasswordOrOtpData { struct PasswordOrOtpData {
#[serde(alias = "MasterPasswordHash")] #[serde(alias = "MasterPasswordHash")]

3
src/auth.rs

@ -1,3 +1,6 @@
#[path = "auth/two_factor.rs"]
pub mod two_factor;
#[path = "auth/send.rs"] #[path = "auth/send.rs"]
pub mod send; pub mod send;
pub type SendTokens = send::SendTokens; pub type SendTokens = send::SendTokens;

221
src/auth/two_factor.rs

@ -0,0 +1,221 @@
use chrono::{TimeDelta, Utc};
use serde::{de::DeserializeOwned, ser::Serialize};
use std::sync::LazyLock;
use crate::{
CONFIG,
api::{ApiResult, EmptyResult},
auth::{decode_jwt, encode_jwt},
db::models::UserId,
};
static JWT_2FA_AUTH_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|api.2fa", CONFIG.domain_origin()));
#[derive(Serialize, Deserialize)]
pub struct TwopFactorClaims<T> {
// Not before
pub nbf: i64,
// Expiration time
pub exp: i64,
// Issuer
pub iss: String,
// Subject
pub sub: UserId,
pub enabled: bool,
pub claims: T,
}
#[derive(Serialize, Deserialize)]
pub struct AuthenticatorClaims {
pub key: String,
}
#[derive(Serialize, Deserialize)]
pub struct DuoClaims {
data: Option<DuoData>,
}
#[derive(Serialize, Deserialize)]
pub struct WebauthnClaims {
pub keys: Vec<i32>,
}
#[derive(Serialize, Deserialize)]
pub struct YubikeyClaims {
pub keys: Vec<String>,
}
#[derive(Serialize, Deserialize, PartialEq)]
pub struct DuoData {
pub host: String, // Duo API hostname
pub ik: String, // client id
pub sk: String, // client secret
}
impl DuoData {
pub fn global() -> Option<Self> {
match (CONFIG._enable_duo(), CONFIG.duo_host()) {
(true, Some(host)) => Some(Self {
host,
ik: CONFIG.duo_ikey().unwrap(),
sk: CONFIG.duo_skey().unwrap(),
}),
_ => None,
}
}
pub fn msg(s: &str) -> Self {
Self {
host: s.into(),
ik: s.into(),
sk: s.into(),
}
}
pub fn secret() -> Self {
Self::msg("<global_secret>")
}
pub fn obscure(self) -> Self {
let mut host = self.host;
let mut ik = self.ik;
let mut sk = self.sk;
let digits = 4;
let replaced = "************";
host.replace_range(digits.., replaced);
ik.replace_range(digits.., replaced);
sk.replace_range(digits.., replaced);
Self {
host,
ik,
sk,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct EmailClaims {
pub email: Option<String>,
}
fn token<T: Serialize>(user_id: UserId, enabled: bool, claims: T) -> String {
let time_now = Utc::now();
let claims = TwopFactorClaims {
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(),
iss: JWT_2FA_AUTH_ISSUER.to_string(),
sub: user_id,
enabled,
claims,
};
encode_jwt(&claims)
}
fn validate<T: DeserializeOwned>(token: &str, user_id: &UserId, enabled: bool) -> ApiResult<T> {
match decode_jwt::<TwopFactorClaims<T>>(token, JWT_2FA_AUTH_ISSUER.to_string()) {
Ok(claims) => {
if claims.sub != *user_id {
err!("Invalid verification token: Invalid user");
}
if claims.enabled != enabled {
err!("Invalid verification token: Invalid state");
}
Ok(claims.claims)
}
Err(err) => err!(format!("Failed to decode verification token: {err}")),
}
}
pub fn authenticator_token(user_id: UserId, key: String, enabled: bool) -> String {
token(
user_id,
enabled,
AuthenticatorClaims {
key,
},
)
}
pub fn validate_authenticator(token: &str, user_id: &UserId, key: &str, enabled: bool) -> EmptyResult {
let claims = validate::<AuthenticatorClaims>(token, user_id, enabled)?;
if claims.key != key {
err!("Invalid verification token: Invalid key");
}
Ok(())
}
pub fn duo_token(user_id: UserId, data: Option<DuoData>, enabled: bool) -> String {
token(
user_id,
enabled,
DuoClaims {
data,
},
)
}
// When disabling we check that it's the correct data
pub fn validate_duo(token: &str, user_id: &UserId, data: Option<&DuoData>, enabled: bool) -> EmptyResult {
let claims = validate::<DuoClaims>(token, user_id, enabled)?;
if enabled && claims.data.as_ref() != data {
err!("Invalid verification token: Invalid duo data");
}
Ok(())
}
pub fn email_token(user_id: UserId, email: Option<String>, enabled: bool) -> String {
token(
user_id,
enabled,
EmailClaims {
email,
},
)
}
// When disabling we check that it's the correct `email`
pub fn validate_email(token: &str, user_id: &UserId, email: String, enabled: bool) -> EmptyResult {
let claims = validate::<EmailClaims>(token, user_id, enabled)?;
if enabled && claims.email != Some(email) {
err!("Invalid verification token: Invalid email");
}
Ok(())
}
pub fn webauthn_token(user_id: UserId, keys: Vec<i32>, enabled: bool) -> String {
token(
user_id,
enabled,
WebauthnClaims {
keys,
},
)
}
pub fn validate_webauthn(token: &str, user_id: &UserId, keys: &[i32], enabled: bool) -> EmptyResult {
let claims = validate::<WebauthnClaims>(token, user_id, enabled)?;
if keys != claims.keys {
err!("Invalid verification token: Invalid keys");
}
Ok(())
}
pub fn yubikey_token(user_id: UserId, keys: Vec<String>, enabled: bool) -> String {
token(
user_id,
enabled,
YubikeyClaims {
keys,
},
)
}
pub fn validate_yubikey(token: &str, user_id: &UserId, keys: &Vec<String>, enabled: bool) -> EmptyResult {
let claims = validate::<YubikeyClaims>(token, user_id, enabled)?;
if *keys != claims.keys {
err!("Invalid verification token: Invalid keys");
}
Ok(())
}

4
src/db/models/two_factor.rs

@ -137,11 +137,11 @@ impl TwoFactor {
.await .await
} }
pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &DbConn) -> Option<Self> { pub async fn find_by_user_and_type(user_uuid: &UserId, atype: TwoFactorType, conn: &DbConn) -> Option<Self> {
conn.run(move |conn| { conn.run(move |conn| {
twofactor::table twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::atype.eq(atype)) .filter(twofactor::atype.eq(atype as i32))
.first::<Self>(conn) .first::<Self>(conn)
.ok() .ok()
}) })

Loading…
Cancel
Save