diff --git a/trifid-api/src/auth.rs b/trifid-api/src/auth.rs index 8b61e68..d58cd46 100644 --- a/trifid-api/src/auth.rs +++ b/trifid-api/src/auth.rs @@ -7,7 +7,7 @@ pub struct PartialUserInfo { pub user_id: i32, pub created_at: i64, pub email: String, - pub has_totp: bool, + pub has_totp_auth: bool, pub session_id: String, pub auth_id: Option @@ -88,7 +88,7 @@ impl<'r> FromRequest<'r> for PartialUserInfo { user_id: user_id as i32, created_at: user.created_on as i64, email: user.email, - has_totp: at.is_some(), + has_totp_auth: at.is_some(), session_id: st, auth_id: at, }) @@ -113,7 +113,7 @@ impl<'r> FromRequest<'r> for TOTPAuthenticatedUserInfo { Outcome::Failure(e) => Outcome::Failure(e), Outcome::Forward(f) => Outcome::Forward(f), Outcome::Success(s) => { - if s.has_totp { + if s.has_totp_auth { Outcome::Success(Self { user_id: s.user_id, created_at: s.created_at, diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 6d80fdc..8d0d2cf 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -1,3 +1,5 @@ +extern crate core; + use std::error::Error; use std::fs; use std::path::Path; @@ -82,7 +84,8 @@ async fn main() -> Result<(), Box> { crate::routes::v1::signup::signup_request, crate::routes::v1::auth::verify_magic_link::verify_magic_link, crate::routes::v1::totp_authenticators::totp_authenticators_request, - crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request + crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request, + crate::routes::v1::auth::totp::totp_request ]) .register("/", catchers![ crate::routes::handler_400, diff --git a/trifid-api/src/routes/v1/auth/mod.rs b/trifid-api/src/routes/v1/auth/mod.rs index aa16fd9..7fce6ff 100644 --- a/trifid-api/src/routes/v1/auth/mod.rs +++ b/trifid-api/src/routes/v1/auth/mod.rs @@ -1,2 +1,3 @@ pub mod verify_magic_link; -pub mod magic_link; \ No newline at end of file +pub mod magic_link; +pub mod totp; \ 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 new file mode 100644 index 0000000..068ae79 --- /dev/null +++ b/trifid-api/src/routes/v1/auth/totp.rs @@ -0,0 +1,66 @@ +use rocket::http::{ContentType, Status}; +use rocket::serde::json::Json; +use crate::auth::PartialUserInfo; +use serde::{Serialize, Deserialize}; +use rocket::{post, State}; +use sqlx::PgPool; +use crate::tokens::{generate_auth_token, get_totpmachine, user_has_totp}; + +pub const TOTP_GENERIC_UNAUTHORIZED_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_INVALID_TOTP_CODE\",\"message\":\"invalid TOTP code (maybe it expired?)\",\"path\":\"code\"}]}"; +pub const TOTP_NO_TOTP_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_NO_TOTP\",\"message\":\"logged-in user does not have totp enabled\",\"path\":\"code\"}]}"; + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpRequest { + pub code: String +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpResponseData { + #[serde(rename = "authToken")] + auth_token: String +} +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpResponseMetadata { +} +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpResponse { + data: TotpResponseData, + metadata: TotpResponseMetadata +} + +#[post("/v1/auth/totp", data = "")] +pub async fn totp_request(req: Json, user: PartialUserInfo, db: &State) -> Result<(ContentType, Json), (Status, String)> { + if !match user_has_totp(user.user_id, db.inner()).await { + Ok(b) => b, + Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e))) + } { + return Err((Status::UnprocessableEntity, TOTP_NO_TOTP_ERROR.to_string())) + } + + if user.has_totp_auth { + return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_AUTHED", "user already has valid totp authentication"))) + } + + let totpmachine = match get_totpmachine(user.user_id, db.inner()).await { + Ok(t) => t, + Err(e) => { + return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e))) + } + }; + + if !totpmachine.check_current(&req.0.code).unwrap_or(false) { + return Err((Status::Unauthorized, TOTP_GENERIC_UNAUTHORIZED_ERROR.to_string())) + } + + Ok((ContentType::JSON, Json(TotpResponse { + data: TotpResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await { + Ok(t) => t, + Err(e) => { return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e))) } + } }, + metadata: TotpResponseMetadata {}, + }))) +} \ No newline at end of file diff --git a/trifid-api/src/routes/v1/totp_authenticators.rs b/trifid-api/src/routes/v1/totp_authenticators.rs index 3d5b207..e803e25 100644 --- a/trifid-api/src/routes/v1/totp_authenticators.rs +++ b/trifid-api/src/routes/v1/totp_authenticators.rs @@ -5,7 +5,7 @@ use sqlx::PgPool; use serde::{Serialize, Deserialize}; use crate::auth::PartialUserInfo; use crate::config::TFConfig; -use crate::tokens::create_totp_token; +use crate::tokens::{create_totp_token, user_has_totp}; #[derive(Deserialize)] pub struct TotpAuthenticatorsRequest {} @@ -29,7 +29,10 @@ pub struct TotpAuthenticatorsResponse { #[post("/v1/totp-authenticators", data = "<_req>")] pub async fn totp_authenticators_request(_req: Json, user: PartialUserInfo, db: &State, config: &State) -> Result<(ContentType, Json), (Status, String)> { - if user.has_totp { + if match user_has_totp(user.user_id, db.inner()).await { + Ok(b) => b, + Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e))) + } { return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_EXISTS", "this user already has a totp authenticator on their account"))) } diff --git a/trifid-api/src/routes/v1/verify_totp_authenticator.rs b/trifid-api/src/routes/v1/verify_totp_authenticator.rs index 3bc5ebb..b6c3bf2 100644 --- a/trifid-api/src/routes/v1/verify_totp_authenticator.rs +++ b/trifid-api/src/routes/v1/verify_totp_authenticator.rs @@ -7,7 +7,6 @@ {"data":{"authToken":"auth-O7mugxdYta-RKtLMqDW4j8XCJ85EfZKKezeZZXBYtFQ"},"metadata":{}} */ -use std::error::Error; use rocket::http::{ContentType, Status}; use crate::auth::PartialUserInfo; use rocket::post; @@ -15,7 +14,6 @@ use rocket::serde::json::Json; use rocket::State; use serde::{Serialize, Deserialize}; use sqlx::PgPool; -use totp_rs::TOTP; use crate::tokens::{generate_auth_token, use_totp_token, verify_totp_token}; #[derive(Serialize, Deserialize)] diff --git a/trifid-api/src/tokens.rs b/trifid-api/src/tokens.rs index 58da308..c0a1c02 100644 --- a/trifid-api/src/tokens.rs +++ b/trifid-api/src/tokens.rs @@ -33,7 +33,7 @@ pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool) Ok(token) } pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box> { - validate_session_token(token.clone(), db).await?; + validate_session_token(session_id.clone(), db).await?; sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?; Ok(()) } @@ -80,4 +80,14 @@ pub async fn use_totp_token(otpid: String, email: String, db: &PgPool) -> Result sqlx::query!("DELETE FROM totp_create_tokens WHERE id = $1", otpid).execute(db).await?; sqlx::query!("UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?; Ok(totpmachine) +} + +pub async fn get_totpmachine(user: i32, db: &PgPool) -> Result> { + let user = sqlx::query!("SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1", user).fetch_one(db).await?; + let secret = Secret::Encoded(user.totp_secret); + Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap()) +} + +pub async fn user_has_totp(user: i32, db: &PgPool) -> Result> { + Ok(sqlx::query!("SELECT totp_verified FROM users WHERE id = $1", user).fetch_one(db).await?.totp_verified == 1) } \ No newline at end of file