Skip to content

Commit

Permalink
feat: adding mfa data apis (#125)
Browse files Browse the repository at this point in the history
* refa: extracting set_otp_secret to a feature
this is just a glimpse of a restructure into feature, db and
router/service layers I will do in the future

* feat: adding mfa info route, which returns the mfa url

* random: adding comment with clippy reason example
  • Loading branch information
nicolasauler authored Aug 26, 2024
1 parent 1f77a78 commit f0f98a9
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 40 deletions.
11 changes: 9 additions & 2 deletions src/data/router/auth.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use std::sync::Arc;

use askama_axum::IntoResponse;
use axum::{extract::State, routing::post, Json, Router};
use axum::{extract::State, routing::get, Json, Router};

use crate::{auth::AuthSession, hypermedia::schema::auth::MfaTokenForm, AppState};

pub fn private_router() -> Router<Arc<AppState>> {
Router::new().route("/api/auth/mfa", post(mfa_verify))
Router::new().route("/api/auth/mfa", get(mfa_info).post(mfa_verify))
}

async fn mfa_info(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
) -> impl IntoResponse {
crate::data::service::auth::mfa_info(auth_session, &shared_state.pool).await
}

async fn mfa_verify(
Expand Down
34 changes: 34 additions & 0 deletions src/data/service/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use axum::{http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};

use crate::{auth::AuthSession, features::totp::set_otp_secret};

#[derive(Serialize, Deserialize)]
pub struct MfaInfo {
pub otp_url: String,
}

pub async fn mfa_info(
auth_session: AuthSession,
db_pool: &Pool<Postgres>,
) -> Result<Json<MfaInfo>, StatusCode> {
let Some(user) = auth_session.user else {
return Err(StatusCode::UNAUTHORIZED);
};
tracing::info!("User logged in");

// TODO: create logic for changing MFA method
if user.otp_enabled {
todo!("Create logic for changing MFA method");
}

let totp = set_otp_secret(db_pool, user.id).await.map_err(|e| {
tracing::error!(?user.id, "Error setting OTP secret: {e}");
return StatusCode::INTERNAL_SERVER_ERROR;
})?;

Ok(Json(MfaInfo {
otp_url: totp.otp_url,
}))
}
1 change: 1 addition & 0 deletions src/data/service/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod auth;
pub mod expenses;
1 change: 1 addition & 0 deletions src/features/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod totp;
48 changes: 48 additions & 0 deletions src/features/totp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::util::generate_otp_token;
use anyhow::bail;
use sqlx::{Pool, Postgres};
use totp_rs::{Algorithm, Secret, TOTP};

pub struct OtpData {
/// `qr_code` is a base64 encoded image that can be rendered embedded in an <img> tag
pub qr_code: String,
/// `otp_url` is a otpauth:// url that can be rendered as a QR code
pub otp_url: String,
}

pub async fn set_otp_secret(db_pool: &Pool<Postgres>, user_id: i32) -> anyhow::Result<OtpData> {
let secret = Secret::Raw(generate_otp_token().as_bytes().to_vec());
let mut transaction = db_pool.begin().await?;

let user_email = sqlx::query!(
r#"
UPDATE users SET otp_secret = $1 WHERE id = $2
RETURNING email
"#,
secret.to_encoded().to_string(),
user_id
)
.fetch_one(&mut *transaction)
.await?;

let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret.to_bytes().unwrap(),
Some("Finnish".to_owned()),
user_email.email,
)?;

transaction.commit().await?;

let Ok(qr_code) = totp.get_qr_base64() else {
bail!("Failed to generate QR code from totp");
};

Ok(OtpData {
qr_code,
otp_url: totp.get_url(),
})
}
54 changes: 16 additions & 38 deletions src/hypermedia/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
frc::{validate_frc, verify_frc_solution},
mail::{send_forgot_password_mail, send_sign_up_confirmation_mail},
},
features::totp::set_otp_secret,
hypermedia::schema::{
auth::MailToUser,
validation::{ChangePasswordInput, Exists, ForgotPasswordInput, ResendEmail, SignUpInput},
Expand Down Expand Up @@ -82,55 +83,32 @@ pub async fn signin(
return (StatusCode::OK, [("HX-Redirect", "/")]).into_response();
}

pub async fn mfa_qr(auth_session: AuthSession, db_pool: &Pool<Postgres>) -> impl IntoResponse {
pub async fn mfa_qr(
auth_session: AuthSession,
db_pool: &Pool<Postgres>,
) -> Result<Response<Body>, Response<Body>> {
let Some(user) = auth_session.user else {
return (StatusCode::UNAUTHORIZED, [("HX-Redirect", "/auth/signin")]).into_response();
return Err((StatusCode::UNAUTHORIZED, [("HX-Redirect", "/auth/signin")]).into_response());
};
let user_id = user.id;
tracing::debug!("User logged in");

// TODO: create logic for changing MFA method
if user.otp_enabled {
// TODO
todo!("Create logic for changing MFA method");
}

let secret = Secret::Raw(generate_otp_token().as_bytes().to_vec());
let mut transaction = db_pool.begin().await.unwrap();

let user_email = sqlx::query!(
r#"
UPDATE users SET otp_secret = $1 WHERE id = $2
RETURNING email
"#,
secret.to_encoded().to_string(),
user_id
)
.fetch_one(&mut *transaction)
.await
.unwrap();

let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret.to_bytes().unwrap(),
Some("Finnish".to_owned()),
user_email.email,
)
.unwrap();
let qr_code = totp.get_qr_base64().unwrap(); // qr_code is a base64 encoded image
// that can be rendered embedded in an <img> tag
let otp_url = totp.get_url(); // otp_url is a otpauth:// url
// that can be rendered as a QR code
let totp = set_otp_secret(db_pool, user.id).await.map_err(|e| {
tracing::error!(?user.id, "Error setting OTP secret: {e}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
})?;

transaction.commit().await.unwrap();
return MfaTemplate {
Ok(MfaTemplate {
mfa_url: "/auth/mfa".to_owned(),
qr_code: format!("data:image/png;base64,{qr_code}"),
otp_auth_url: otp_url,
qr_code: format!("data:image/png;base64,{}", totp.qr_code),
otp_auth_url: totp.otp_url,
..Default::default()
}
.into_response_with_nonce();
.into_response_with_nonce())
}

pub async fn mfa_verify(
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod client;
mod constant;
mod data;
mod data_structs;
mod features;
mod hypermedia;
/// Module containing the database schemas and i/o schemas for hypermedia and data apis.
mod schema;
Expand Down
4 changes: 4 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ pub struct UpdateExpense {

/// Function to set the default value for `is_essential` in `UpdateExpense` to be Some(false).
#[allow(clippy::unnecessary_wraps)]
//#[allow(
// clippy::unnecessary_wraps,
// reason = "Needs to return option for custom deserializer"
//)]
const fn default_is_essential_to_false() -> Option<bool> {
return Some(false);
}
Expand Down

0 comments on commit f0f98a9

Please sign in to comment.