From 19332e519bf090427c1fe8878ab3930eed09da18 Mon Sep 17 00:00:00 2001 From: core Date: Wed, 22 Nov 2023 10:59:21 -0500 Subject: [PATCH] totp auth work --- trifid-api/config.toml | 7 +- .../down.sql | 1 + .../up.sql | 6 + trifid-api/src/config.rs | 1 + trifid-api/src/main.rs | 2 + trifid-api/src/models.rs | 12 ++ trifid-api/src/routes/v1/auth/totp.rs | 69 +++++++++++- .../src/routes/v1/auth/verify_magic_link.rs | 2 +- trifid-api/src/routes/v1/mod.rs | 1 + .../routes/v1/verify_totp_authenticator.rs | 103 ++++++++++++++++++ trifid-api/src/schema.rs | 10 ++ 11 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 trifid-api/migrations/2023-11-22-153303_create_auth_tokens/down.sql create mode 100644 trifid-api/migrations/2023-11-22-153303_create_auth_tokens/up.sql create mode 100644 trifid-api/src/routes/v1/verify_totp_authenticator.rs diff --git a/trifid-api/config.toml b/trifid-api/config.toml index d78483d..aabe030 100644 --- a/trifid-api/config.toml +++ b/trifid-api/config.toml @@ -32,7 +32,7 @@ from_name = "Trifid" # (Required) The address to send the email from from_email = "core@e3t.cc" # (Required) The email template to use. %TOKEN% will be replaced with the magic link token. -template = "Click this link to sign in! http://localhost:5173/magic-link?magicLinkToken=%TOKEN%" +template = "Click this link to sign in! http://localhost:5173/magic-link?magicLinkToken=%TOKEN%\nAlternatively, use `%TOKEN%` with tfcli to log in." # (Required) Should STARTTLS be used? starttls = false @@ -42,4 +42,7 @@ starttls = false magic_link_expiry_seconds = 3600 # 1 hour # (Required) How long should session tokens be valid for, in seconds? This controls how long users can remain "identified" # before they must re-identify via magic link. -session_token_expiry_seconds = 31536000 # ~1 year \ No newline at end of file +session_token_expiry_seconds = 31536000 # ~1 year +# (Required) How long should auth tokens be valid for, in seconds? This controls how long users can remain logged in +# before they must re-authenticate via 2FA. +auth_token_expiry_seconds = 86400 # 24 hours \ No newline at end of file diff --git a/trifid-api/migrations/2023-11-22-153303_create_auth_tokens/down.sql b/trifid-api/migrations/2023-11-22-153303_create_auth_tokens/down.sql new file mode 100644 index 0000000..21d6894 --- /dev/null +++ b/trifid-api/migrations/2023-11-22-153303_create_auth_tokens/down.sql @@ -0,0 +1 @@ +DROP TABLE auth_tokens; \ No newline at end of file diff --git a/trifid-api/migrations/2023-11-22-153303_create_auth_tokens/up.sql b/trifid-api/migrations/2023-11-22-153303_create_auth_tokens/up.sql new file mode 100644 index 0000000..9e7b6b5 --- /dev/null +++ b/trifid-api/migrations/2023-11-22-153303_create_auth_tokens/up.sql @@ -0,0 +1,6 @@ +CREATE TABLE auth_tokens +( + id VARCHAR NOT NULL PRIMARY KEY, + user_id VARCHAR NOT NULL REFERENCES users (id) ON DELETE CASCADE, + expires TIMESTAMP NOT NULL +); \ No newline at end of file diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index 5dc89ce..66bb4ec 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -42,4 +42,5 @@ pub struct ConfigEmail { pub struct ConfigTokens { pub magic_link_expiry_seconds: u64, pub session_token_expiry_seconds: u64, + pub auth_token_expiry_seconds: u64 } diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 336b609..5cbdb97 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -134,6 +134,8 @@ async fn main() { .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) + .service(routes::v1::verify_totp_authenticator::verify_totp_req) + .service(routes::v1::auth::totp::totp_req) .wrap(Logger::default()) .wrap(actix_cors::Cors::permissive()) .app_data(app_state.clone()) diff --git a/trifid-api/src/models.rs b/trifid-api/src/models.rs index edd45b5..a2e5ea4 100644 --- a/trifid-api/src/models.rs +++ b/trifid-api/src/models.rs @@ -45,3 +45,15 @@ pub struct TotpAuthenticator { pub secret: String, pub verified: bool, } + +#[derive( +Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +)] +#[diesel(belongs_to(User))] +#[diesel(table_name = crate::schema::auth_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct AuthToken { + pub id: String, + pub user_id: String, + pub expires: SystemTime, +} \ No newline at end of file diff --git a/trifid-api/src/routes/v1/auth/totp.rs b/trifid-api/src/routes/v1/auth/totp.rs index dcd75db..4a7355a 100644 --- a/trifid-api/src/routes/v1/auth/totp.rs +++ b/trifid-api/src/routes/v1/auth/totp.rs @@ -1,21 +1,82 @@ +use std::time::{Duration, SystemTime}; +use actix_web::http::StatusCode; +use actix_web::{HttpRequest, post}; +use actix_web::web::{Data, Json}; use serde::{Deserialize, Serialize}; +use crate::{AppState, auth, enforce, randid}; +use crate::response::JsonAPIResponse; +use diesel::{QueryDsl, ExpressionMethods, SelectableHelper, BelongingToDsl}; +use diesel_async::RunQueryDsl; +use totp_rs::{Algorithm, Secret, TOTP}; +use crate::schema::{auth_tokens, users}; +use crate::models::{AuthToken, TotpAuthenticator, User}; -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct TotpAuthReq { pub code: String } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct TotpAuthRespMeta {} -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct TotpAuthRespData { #[serde(rename = "authToken")] pub auth_token: String, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct TotpAuthResp { pub data: TotpAuthRespData, pub metadata: TotpAuthRespMeta, +} + +#[post("/v1/auth/totp")] +pub async fn totp_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); + let session_token = enforce!(sess auth_info); + let user = handle_error!(users::table.find(&session_token.user_id).first::(&mut conn).await); + + let authenticators: Vec = handle_error!(TotpAuthenticator::belonging_to(&user).load::(&mut conn).await); + + let mut found_valid_code = false; + + for totp_auther in authenticators { + if totp_auther.verified { + let secret = Secret::Encoded(totp_auther.secret); + let totp_machine = handle_error!(TOTP::new(Algorithm::SHA1, 6, 1, 30, handle_error!(secret.to_bytes()), Some("Trifid".to_string()), user.email.clone())); + let is_valid = handle_error!(totp_machine.check_current(&req.code)); + if is_valid { found_valid_code = true; break; } + } + } + if !found_valid_code { + err!(StatusCode::UNAUTHORIZED, make_err!("ERR_UNAUTHORIZED", "unauthorized")); + } + + // issue auth token + + let new_token = AuthToken { + id: randid!(token "auth"), + user_id: user.id.clone(), + expires: SystemTime::now() + + Duration::from_secs(state.config.tokens.auth_token_expiry_seconds), + }; + + handle_error!( + diesel::insert_into(auth_tokens::table) + .values(&new_token) + .execute(&mut conn) + .await + ); + + ok!( + TotpAuthResp { + data: TotpAuthRespData { + auth_token: new_token.id.clone() + }, + metadata: TotpAuthRespMeta {} + } + ) } \ No newline at end of file diff --git a/trifid-api/src/routes/v1/auth/verify_magic_link.rs b/trifid-api/src/routes/v1/auth/verify_magic_link.rs index e09da20..c4b0433 100644 --- a/trifid-api/src/routes/v1/auth/verify_magic_link.rs +++ b/trifid-api/src/routes/v1/auth/verify_magic_link.rs @@ -5,7 +5,7 @@ use crate::{randid, AppState}; use actix_web::http::StatusCode; use actix_web::post; use actix_web::web::{Data, Json}; -use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; +use diesel::QueryDsl; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use diesel::result::OptionalExtension; diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index 40ce087..1abc840 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; +pub mod verify_totp_authenticator; diff --git a/trifid-api/src/routes/v1/verify_totp_authenticator.rs b/trifid-api/src/routes/v1/verify_totp_authenticator.rs new file mode 100644 index 0000000..8069d37 --- /dev/null +++ b/trifid-api/src/routes/v1/verify_totp_authenticator.rs @@ -0,0 +1,103 @@ +use std::time::{Duration, SystemTime}; +use actix_web::http::StatusCode; +use actix_web::{HttpRequest, post}; +use actix_web::web::{Data, Json}; +use serde::{Deserialize, Serialize}; +use crate::{AppState, auth, enforce, randid}; +use crate::response::JsonAPIResponse; +use diesel::{QueryDsl, ExpressionMethods, SelectableHelper, OptionalExtension}; +use diesel_async::RunQueryDsl; +use totp_rs::{Algorithm, Secret, TOTP}; +use crate::schema::{auth_tokens, totp_authenticators, users}; +use crate::models::{AuthToken, TotpAuthenticator, User}; + +#[derive(Deserialize, Debug)] +pub struct VerifyTotpAuthReq { + #[serde(rename = "totpToken")] + pub totp_token: String, + pub code: String +} + +#[derive(Serialize, Debug)] +pub struct TotpAuthRespMeta {} + +#[derive(Serialize, Debug)] +pub struct TotpAuthRespData { + #[serde(rename = "authToken")] + pub auth_token: String, +} + +#[derive(Serialize, Debug)] +pub struct TotpAuthResp { + pub data: TotpAuthRespData, + pub metadata: TotpAuthRespMeta, +} + +#[post("/v1/verify-totp-authenticator")] +pub async fn verify_totp_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); + let session_token = enforce!(sess auth_info); + let user = handle_error!(users::table.find(&session_token.user_id).first::(&mut conn).await); + + let authenticator = match handle_error!(totp_authenticators::table.find(&req.totp_token).first::(&mut conn).await.optional()) { + Some(t) => t, + None => { + err!( + StatusCode::BAD_REQUEST, + make_err!( + "ERR_INVALID_TOTP_TOKEN", + "TOTP token does not exist (maybe it expired?)", + "totpToken" + ) + ) + } + }; + + if authenticator.verified { + err!( + StatusCode::BAD_REQUEST, + make_err!( + "ERR_INVALID_TOTP_TOKEN", + "TOTP token already verified", + "totpToken" + ) + ); + } + + let secret = Secret::Encoded(authenticator.secret.clone()); + let totp_machine = handle_error!(TOTP::new(Algorithm::SHA1, 6, 1, 30, handle_error!(secret.to_bytes()), Some("Trifid".to_string()), user.email.clone())); + let is_valid = handle_error!(totp_machine.check_current(&req.code)); + + if !is_valid { + err!(StatusCode::UNAUTHORIZED, make_err!("ERR_UNAUTHORIZED", "unauthorized")); + } + + handle_error!(diesel::update(&authenticator).set(totp_authenticators::dsl::verified.eq(true)).execute(&mut conn).await); + + // issue auth token + + let new_token = AuthToken { + id: randid!(token "auth"), + user_id: user.id.clone(), + expires: SystemTime::now() + + Duration::from_secs(state.config.tokens.auth_token_expiry_seconds), + }; + + handle_error!( + diesel::insert_into(auth_tokens::table) + .values(&new_token) + .execute(&mut conn) + .await + ); + + ok!( + TotpAuthResp { + data: TotpAuthRespData { + auth_token: new_token.id.clone() + }, + metadata: TotpAuthRespMeta {} + } + ) +} \ No newline at end of file diff --git a/trifid-api/src/schema.rs b/trifid-api/src/schema.rs index f308eaa..0e22283 100644 --- a/trifid-api/src/schema.rs +++ b/trifid-api/src/schema.rs @@ -1,5 +1,13 @@ // @generated automatically by Diesel CLI. +diesel::table! { + auth_tokens (id) { + id -> Varchar, + user_id -> Varchar, + expires -> Timestamp, + } +} + diesel::table! { magic_links (id) { id -> Varchar, @@ -32,11 +40,13 @@ diesel::table! { } } +diesel::joinable!(auth_tokens -> users (user_id)); diesel::joinable!(magic_links -> users (user_id)); diesel::joinable!(session_tokens -> users (user_id)); diesel::joinable!(totp_authenticators -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( + auth_tokens, magic_links, session_tokens, totp_authenticators,