expose 2fa publicly

This commit is contained in:
aecsocket
2026-08-18 02:33:39 +09:00
parent 760a1adbe4
commit d192fb805b
5 changed files with 126 additions and 127 deletions
@@ -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"
}
@@ -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"
}
+1
View File
@@ -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,
+69
View File
@@ -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<bool, AuthenticationError> {
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<bool, AuthenticationError> {
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())
}
+33 -112
View File
@@ -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<bool, AuthenticationError> {
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,
)));