diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index b9a8420..9b454d7 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -61,7 +61,9 @@ pub struct TrifidConfigTokens { #[serde(default = "session_token_expiry_time")] pub session_token_expiry_time_seconds: u64, #[serde(default = "totp_setup_timeout_time")] - pub totp_setup_timeout_time_seconds: u64 + pub totp_setup_timeout_time_seconds: u64, + #[serde(default = "mfa_tokens_expiry_time")] + pub mfa_tokens_expiry_time_seconds: u64 } fn max_connections_default() -> u32 { 100 } @@ -71,4 +73,5 @@ fn sqlx_logging_default() -> bool { true } fn socketaddr_8080() -> SocketAddr { SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080)) } fn magic_link_expiry_time() -> u64 { 3600 } // 1 hour fn session_token_expiry_time() -> u64 { 15780000 } // 6 months -fn totp_setup_timeout_time() -> u64 { 600 } // 10 minutes \ No newline at end of file +fn totp_setup_timeout_time() -> u64 { 600 } // 10 minutes +fn mfa_tokens_expiry_time() -> u64 { 600 } // 10 minutes \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 4218f86..f54aca5 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -66,6 +66,7 @@ async fn main() -> Result<(), Box> { .service(routes::v1::signup::signup_request) .service(routes::v1::auth::verify_magic_link::verify_magic_link_request) .service(routes::v1::totp_authenticators::totp_authenticators_request) + .service(routes::v1::verify_totp_authenticators::verify_totp_authenticators_request) }).bind(CONFIG.server.bind)?.run().await?; Ok(()) diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index 65f6347..36a9ce5 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod signup; -pub mod totp_authenticators; \ No newline at end of file +pub mod totp_authenticators; +pub mod verify_totp_authenticators; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/verify_totp_authenticators.rs b/trifid-api/src/routes/v1/verify_totp_authenticators.rs new file mode 100644 index 0000000..2228106 --- /dev/null +++ b/trifid-api/src/routes/v1/verify_totp_authenticators.rs @@ -0,0 +1,198 @@ +use actix_web::{HttpRequest, HttpResponse, post}; +use actix_web::web::{Data, Json}; +use log::error; +use serde::{Serialize, Deserialize}; +use trifid_api_entities::entity::totp_authenticator; +use crate::AppState; +use crate::auth_tokens::{enforce_session, TokenInfo}; +use crate::error::{APIError, APIErrorsResponse}; +use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, IntoActiveModel, ActiveModelTrait}; +use sea_orm::ActiveValue::Set; +use totp_rs::{Algorithm, Secret, TOTP}; +use trifid_api_entities::entity::auth_token; +use crate::config::CONFIG; +use crate::timers::expires_in_seconds; +use crate::tokens::random_token; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VerifyTotpAuthenticatorsRequest { + #[serde(rename = "totpToken")] + pub totp_token: String, + pub code: String +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VerifyTotpAuthenticatorsResponse { + pub data: VerifyTotpAuthenticatorsResponseData, + pub metadata: VerifyTotpAuthenticatorsResponseMetadata +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VerifyTotpAuthenticatorsResponseData { + #[serde(rename = "authToken")] + pub auth_token: String +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VerifyTotpAuthenticatorsResponseMetadata {} + +#[post("/v1/verify-totp-authenticators")] +pub async fn verify_totp_authenticators_request(req: Json, req_data: HttpRequest, db: Data) -> HttpResponse { + // require a user session + let session_token = match enforce_session(&req_data, &db.conn).await { + Ok(r) => { + match r { + TokenInfo::SessionToken(i) => i, + _ => unreachable!() + } + } + Err(e) => { + error!("error enforcing session: {}", e); + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_UNAUTHORIZED".to_string(), + message: "Unauthorized".to_string(), + path: None, + } + ], + }); + } + }; + + // determine if the user has a totp authenticator + let auther = match totp_authenticator::Entity::find().filter(totp_authenticator::Column::User.eq(&session_token.user.id)).one(&db.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error with the database request, please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + let auther = match auther { + Some(a) => { + if a.verified { + return HttpResponse::BadRequest().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_ALREADY_HAS_TOTP".to_string(), + message: "This user already has a totp authenticator".to_string(), + path: None, + } + ] + }); + } + a + }, + None => { + return HttpResponse::BadRequest().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_USER_NO_TOTP".to_string(), + message: "This user does not have a totp authenticator".to_string(), + path: None, + } + ] + }); + } + }; + + let secret = Secret::Encoded(auther.secret.clone()); + let totpmachine = match TOTP::from_url(auther.url.clone()) { + Ok(m) => m, + Err(e) => { + error!("totp url error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_SECRET_ERROR".to_string(), + message: "There was an error parsing the totpmachine. Please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + let valid = match totpmachine.check_current(&req.totp_token) { + Ok(valid) => valid, + Err(e) => { + error!("system time error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_TIME_ERROR".to_string(), + message: "There was an with the server-side time clock.".to_string(), + path: None, + } + ], + }); + } + }; + + if !valid { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_UNAUTHORIZED".to_string(), + message: "Unauthorized".to_string(), + path: None, + } + ], + }) + } + + let mut active_model = auther.into_active_model(); + + active_model.verified = Set(true); + + match active_model.update(&db.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error updating the totpmachine, please try again later.".to_string(), + path: None, + } + ], + }) + } + } + + let model: auth_token::Model = auth_token::Model { + id: random_token("auth"), + user: session_token.user.id, + expires_on: expires_in_seconds(CONFIG.tokens.mfa_tokens_expiry_time_seconds) as i64, + }; + let token = model.id.clone(); + let active_model = model.into_active_model(); + match active_model.insert(&db.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error issuing the authentication token.".to_string(), + path: None, + } + ], + }); + } + } + + HttpResponse::Ok().json(VerifyTotpAuthenticatorsResponse { + data: VerifyTotpAuthenticatorsResponseData { auth_token: token }, + metadata: VerifyTotpAuthenticatorsResponseMetadata {}, + }) +} \ No newline at end of file