diff --git a/apps/labrinth/.sqlx/query-c9dd036ae76392c521a7da922935496de1fee24672bc5b8d4d703003ba57482f.json b/apps/labrinth/.sqlx/query-c9dd036ae76392c521a7da922935496de1fee24672bc5b8d4d703003ba57482f.json new file mode 100644 index 0000000000..dc349bb21a --- /dev/null +++ b/apps/labrinth/.sqlx/query-c9dd036ae76392c521a7da922935496de1fee24672bc5b8d4d703003ba57482f.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tDELETE FROM user_backup_codes\n\t\tWHERE user_id = $1 AND code = $2\n\t\tRETURNING code\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "code", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c9dd036ae76392c521a7da922935496de1fee24672bc5b8d4d703003ba57482f" +} diff --git a/apps/labrinth/.sqlx/query-de1bf7e33a99a10154cefdbe3b8322e4c6a19448b6ee3c6087b1b8163bc52cb1.json b/apps/labrinth/.sqlx/query-de1bf7e33a99a10154cefdbe3b8322e4c6a19448b6ee3c6087b1b8163bc52cb1.json deleted file mode 100644 index 7a48589fc6..0000000000 --- a/apps/labrinth/.sqlx/query-de1bf7e33a99a10154cefdbe3b8322e4c6a19448b6ee3c6087b1b8163bc52cb1.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM user_backup_codes\n WHERE user_id = $1 AND code = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "de1bf7e33a99a10154cefdbe3b8322e4c6a19448b6ee3c6087b1b8163bc52cb1" -} diff --git a/apps/labrinth/src/auth/mod.rs b/apps/labrinth/src/auth/mod.rs index c58a032f13..7386a38cff 100644 --- a/apps/labrinth/src/auth/mod.rs +++ b/apps/labrinth/src/auth/mod.rs @@ -1,6 +1,7 @@ pub mod checks; pub mod oauth; pub mod templates; +pub mod two_factor; pub mod validate; pub use checks::{ filter_enlisted_projects_ids, filter_enlisted_version_ids, diff --git a/apps/labrinth/src/auth/two_factor.rs b/apps/labrinth/src/auth/two_factor.rs new file mode 100644 index 0000000000..a60421a5f5 --- /dev/null +++ b/apps/labrinth/src/auth/two_factor.rs @@ -0,0 +1,69 @@ +use ariadne::ids::base62_impl::parse_base62; +use xredis::RedisPool; + +use super::AuthenticationError; +use crate::database::{PgTransaction, models::DBUserId}; + +const TOTP_NAMESPACE: &str = "used_totp:v4"; + +pub async fn verify_2fa_code( + input: &str, + secret: &str, + user_id: DBUserId, + redis: &RedisPool, +) -> Result { + let totp = totp_rs::TOTP::new( + totp_rs::Algorithm::SHA1, + 6, + 1, + 30, + totp_rs::Secret::Encoded(secret.to_owned()) + .to_bytes() + .map_err(|_| AuthenticationError::InvalidCredentials)?, + ) + .map_err(|_| AuthenticationError::InvalidCredentials)?; + + let mut conn = redis.connect().await?; + let logical_key = format!("{input}-{}", user_id.0); + let key = redis + .key() + .with_slot(TOTP_NAMESPACE, &logical_key, &logical_key); + + if conn.get(&key).await?.is_some() { + return Err(AuthenticationError::InvalidCredentials); + } + + let is_valid = totp + .check_current(input) + .map_err(|_| AuthenticationError::InvalidCredentials)?; + + if is_valid { + conn.set(&key, "", Some(60)).await?; + } + + Ok(is_valid) +} + +pub async fn use_backup_code( + input: &str, + user_id: DBUserId, + transaction: &mut PgTransaction<'_>, +) -> Result { + let Ok(code) = parse_base62(input) else { + return Ok(false); + }; + + let deleted = sqlx::query_scalar!( + r#" + DELETE FROM user_backup_codes + WHERE user_id = $1 AND code = $2 + RETURNING code + "#, + user_id.0, + code as i64, + ) + .fetch_optional(&mut *transaction) + .await?; + + Ok(deleted.is_some()) +} diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 7f33c7363c..0faf3374c6 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1,3 +1,4 @@ +use crate::auth::two_factor::{use_backup_code, verify_2fa_code}; use crate::auth::validate::{ get_full_user_from_headers, get_user_record_from_bearer_token, }; @@ -2303,79 +2304,6 @@ pub struct Login2FA { pub flow: String, } -async fn validate_2fa_code( - input: String, - secret: String, - allow_backup: bool, - user_id: crate::database::models::DBUserId, - redis: &RedisPool, - pool: &PgPool, - transaction: &mut PgTransaction<'_>, -) -> Result { - let totp = totp_rs::TOTP::new( - totp_rs::Algorithm::SHA1, - 6, - 1, - 30, - totp_rs::Secret::Encoded(secret) - .to_bytes() - .map_err(|_| AuthenticationError::InvalidCredentials)?, - ) - .map_err(|_| AuthenticationError::InvalidCredentials)?; - - const TOTP_NAMESPACE: &str = "used_totp:v4"; - let mut conn = redis.connect().await?; - let logical_key = format!("{}-{}", input, user_id.0); - let key = redis - .key() - .with_slot(TOTP_NAMESPACE, &logical_key, &logical_key); - - // Check if TOTP has already been used - if conn.get(&key).await?.is_some() { - return Err(AuthenticationError::InvalidCredentials); - } - - if totp - .check_current(input.as_str()) - .map_err(|_| AuthenticationError::InvalidCredentials)? - { - conn.set(&key, "", Some(60)).await?; - - Ok(true) - } else if allow_backup { - let backup_codes = - crate::database::models::DBUser::get_backup_codes(user_id, pool) - .await?; - - if !backup_codes.contains(&input) { - Ok(false) - } else { - let code = parse_base62(&input).unwrap_or_default(); - - sqlx::query!( - " - DELETE FROM user_backup_codes - WHERE user_id = $1 AND code = $2 - ", - user_id as crate::database::models::ids::DBUserId, - code as i64, - ) - .execute(&mut *transaction) - .await?; - - crate::database::models::DBUser::clear_caches( - &[(user_id, None)], - redis, - ) - .await?; - - Ok(true) - } - } else { - Err(AuthenticationError::InvalidCredentials) - } -} - /// Complete login with 2FA. #[utoipa::path( context_path = "/auth", @@ -2412,20 +2340,21 @@ pub async fn login_2fa( .begin() .await .wrap_internal_err("starting database transaction")?; - if !validate_2fa_code( - login.code.clone(), - user.totp_secret - .ok_or_else(|| AuthenticationError::InvalidCredentials) - .wrap_auth_err("authenticating API request")?, - true, - user.id, - &redis, - &pool, - &mut transaction, - ) - .await - .wrap_auth_err("authenticating API request")? - { + let secret = user + .totp_secret + .ok_or_else(|| AuthenticationError::InvalidCredentials) + .wrap_auth_err("authenticating API request")?; + let valid_totp = verify_2fa_code(&login.code, &secret, user.id, &redis) + .await + .wrap_auth_err("authenticating API request")?; + let valid_backup = if valid_totp { + false + } else { + use_backup_code(&login.code, user.id, &mut transaction) + .await + .wrap_auth_err("authenticating API request")? + }; + if !valid_totp && !valid_backup { return Err(ApiError::Auth(eyre::eyre!( AuthenticationError::InvalidCredentials, ))); @@ -2554,17 +2483,9 @@ pub async fn finish_2fa_flow( .await .wrap_internal_err("starting database transaction")?; - if !validate_2fa_code( - login.code.clone(), - secret.clone(), - false, - user.id.into(), - &redis, - &pool, - &mut transaction, - ) - .await - .wrap_auth_err("authenticating API request")? + if !verify_2fa_code(&login.code, &secret, user.id.into(), &redis) + .await + .wrap_auth_err("authenticating API request")? { return Err(ApiError::Auth(eyre::eyre!( AuthenticationError::InvalidCredentials, @@ -2703,20 +2624,20 @@ pub async fn remove_2fa( .await .wrap_internal_err("starting database transaction")?; - if !validate_2fa_code( - login.code.clone(), - user.totp_secret.wrap_request_err_with(|| { - "user does not have 2FA enabled on the account!".to_string() - })?, - true, - user.id, - &redis, - &pool, - &mut transaction, - ) - .await - .wrap_auth_err("authenticating API request")? - { + let secret = user.totp_secret.wrap_request_err_with(|| { + "user does not have 2FA enabled on the account!".to_string() + })?; + let valid_totp = verify_2fa_code(&login.code, &secret, user.id, &redis) + .await + .wrap_auth_err("authenticating API request")?; + let valid_backup = if valid_totp { + false + } else { + use_backup_code(&login.code, user.id, &mut transaction) + .await + .wrap_auth_err("authenticating API request")? + }; + if !valid_totp && !valid_backup { return Err(ApiError::Auth(eyre::eyre!( AuthenticationError::InvalidCredentials, )));