diff --git a/Cargo.lock b/Cargo.lock index 00bb8ed..a7285a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -650,6 +656,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + [[package]] name = "convert_case" version = "0.4.0" @@ -2902,6 +2914,22 @@ dependencies = [ "winnow", ] +[[package]] +name = "totp-rs" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3504f96adf86d28e7eb16fa236a7951ec72c15ee100d1b5318e225944bc8cb" +dependencies = [ + "base32", + "constant_time_eq 0.2.6", + "hmac", + "rand", + "sha1", + "sha2", + "url", + "urlencoding", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2957,6 +2985,7 @@ dependencies = [ "serde", "serde_json", "toml 0.8.5", + "totp-rs", ] [[package]] @@ -3037,6 +3066,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3417,7 +3452,7 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", diff --git a/trifid-api/Cargo.toml b/trifid-api/Cargo.toml index 595c9c9..1d56761 100644 --- a/trifid-api/Cargo.toml +++ b/trifid-api/Cargo.toml @@ -24,4 +24,5 @@ diesel-async = { version = "0.4", features = ["postgres", "bb8", "async-connecti diesel_migrations = "2" bb8 = "0.8" rand = "0.8" -mail-send = "0.4" \ No newline at end of file +mail-send = "0.4" +totp-rs = { version = "5.4", features = ["gen_secret", "otpauth"] } \ No newline at end of file diff --git a/trifid-api/src/auth.rs b/trifid-api/src/auth.rs index 3d650ee..fbb019d 100644 --- a/trifid-api/src/auth.rs +++ b/trifid-api/src/auth.rs @@ -61,36 +61,61 @@ macro_rules! auth { #[macro_export] macro_rules! enforce { - (sess $i:expr) => { + (sess $i:expr) => {{ if $i.session_token.is_none() { $crate::err!( actix_web::http::StatusCode::UNAUTHORIZED, $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") ) } - }; - (auth $i:expr) => { + $i.session_token.unwrap() + }}; + (auth $i:expr) => {{ if $i.auth_token.is_none() { $crate::err!( actix_web::http::StatusCode::UNAUTHORIZED, - $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") + $crate::make_err!( + "ERR_2FA_REQUIRED", + "must provide a valid 2FA token to access this endpoint" + ) ) } - }; - (sess auth $i:expr) => { - if $i.session_token.is_none() || $i.auth_token.is_none() { + $i.auth_token.unwrap() + }}; + (sess auth $i:expr) => {{ + if $i.session_token.is_none() { $crate::err!( actix_web::http::StatusCode::UNAUTHORIZED, $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") ) } - }; - (auth sess $i:expr) => { - if $i.session_token.is_none() || $i.auth_token.is_none() { + if $i.auth_token.is_none() { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!( + "ERR_2FA_REQUIRED", + "must provide a valid 2FA token to access this endpoint" + ) + ) + } + ($i.session_token.unwrap(), $i.auth_token.unwrap()) + }}; + (auth sess $i:expr) => {{ + if $i.session_token.is_none() { $crate::err!( actix_web::http::StatusCode::UNAUTHORIZED, $crate::make_err!("ERR_UNAUTHORIZED", "unauthorized") ) } - }; + if $i.auth_token.is_none() { + $crate::err!( + actix_web::http::StatusCode::UNAUTHORIZED, + $crate::make_err!( + "ERR_2FA_REQUIRED", + "must provide a valid 2FA token to access this endpoint" + ) + ) + } + ($i.session_token.unwrap(), $i.auth_token.unwrap()) + }}; } diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 47df947..336b609 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -133,6 +133,7 @@ async fn main() { .service(routes::v1::signup::signup_req) .service(routes::v1::auth::verify_magic_link::verify_link_req) .service(routes::v1::auth::magic_link::login_req) + .service(routes::v1::totp_authenticators::create_totp_auth_req) .wrap(Logger::default()) .wrap(actix_cors::Cors::permissive()) .app_data(app_state.clone()) diff --git a/trifid-api/src/routes/v1/totp_authenticators.rs b/trifid-api/src/routes/v1/totp_authenticators.rs index 070e1b6..78ad6bf 100644 --- a/trifid-api/src/routes/v1/totp_authenticators.rs +++ b/trifid-api/src/routes/v1/totp_authenticators.rs @@ -1,5 +1,6 @@ +use crate::models::TotpAuthenticator; use crate::response::JsonAPIResponse; -use crate::{auth, enforce, AppState}; +use crate::{auth, enforce, randid, AppState}; use actix_web::web::{Data, Json}; use actix_web::{post, HttpRequest}; use diesel::ExpressionMethods; @@ -7,6 +8,10 @@ use diesel::QueryDsl; use diesel::SelectableHelper; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; +use totp_rs::{Algorithm, Secret, TOTP}; +use crate::schema::totp_authenticators; +use crate::schema::users; +use crate::models::User; #[derive(Deserialize)] pub struct TotpAuthenticatorReq {} @@ -28,16 +33,50 @@ pub struct TotpAuthResp { pub metadata: TotpAuthRespMeta, } -#[post("/v1/auth/totp-authenticators")] -pub async fn totp_auth_req( - req: Json, +#[post("/v1/totp-authenticators")] +pub async fn create_totp_auth_req( + _req: Json, state: Data, req_info: HttpRequest, ) -> JsonAPIResponse { let mut conn = handle_error!(state.pool.get().await); let auth_info = auth!(req_info, conn); - enforce!(sess auth_info); + let session_token = enforce!(sess auth_info); - todo!() + let users_vec = handle_error!( + users::dsl::users + .filter(users::dsl::id.eq(&session_token.user_id)) + .select(User::as_select()) + .load(&mut conn) + .await + ); + let user = handle_error!(users_vec.get(0).ok_or("impossible relation")); + + let secret = Secret::generate_secret(); + + let totp = handle_error!(TOTP::new(Algorithm::SHA1, 6, 1, 30, handle_error!(secret.to_bytes()), Some("Trifid".to_string()), user.email.clone())); + + let new_totp_authenticator = TotpAuthenticator { + id: randid!(id "totp"), + user_id: session_token.user_id, + secret: secret.to_encoded().to_string(), + verified: false + }; + + handle_error!( + diesel::insert_into(totp_authenticators::table) + .values(&new_totp_authenticator) + .execute(&mut conn) + .await + ); + + ok!(TotpAuthResp { + data: TotpAuthRespData { + totp_token: new_totp_authenticator.id.clone(), + secret: new_totp_authenticator.secret.clone(), + url: totp.get_url() + }, + metadata: TotpAuthRespMeta {} + }) }