diff --git a/trifid-api/config.toml b/trifid-api/config.toml index 655b64e..720184e 100644 --- a/trifid-api/config.toml +++ b/trifid-api/config.toml @@ -2,4 +2,5 @@ listen_port = 8000 db_url = "postgres://postgres@localhost/trifidapi" base = "http://localhost:8000" magic_links_valid_for = 86400 -session_tokens_valid_for = 86400 \ No newline at end of file +session_tokens_valid_for = 86400 +totp_verification_valid_for = 3600 \ No newline at end of file diff --git a/trifid-api/migrations/20230207145453_create_totp_tokens.sql b/trifid-api/migrations/20230207145453_create_totp_tokens.sql new file mode 100644 index 0000000..c484e94 --- /dev/null +++ b/trifid-api/migrations/20230207145453_create_totp_tokens.sql @@ -0,0 +1,6 @@ +CREATE TABLE totp_create_tokens ( + id VARCHAR(41) NOT NULL PRIMARY KEY, + expires_on INTEGER NOT NULL, + totp_otpurl VARCHAR(3000) NOT NULL, + totp_secret VARCHAR(128) NOT NULL +); \ No newline at end of file diff --git a/trifid-api/src/auth.rs b/trifid-api/src/auth.rs index 0ee5793..ad8ef17 100644 --- a/trifid-api/src/auth.rs +++ b/trifid-api/src/auth.rs @@ -1,14 +1,13 @@ use rocket::http::Status; -use rocket::{Request, State}; +use rocket::{Request}; use rocket::request::{FromRequest, Outcome}; -use sqlx::PgPool; use crate::tokens::{validate_auth_token, validate_session_token}; pub struct PartialUserInfo { pub user_id: i32, pub created_at: i64, pub email: String, - pub hasTotp: bool + pub has_totp: bool } #[derive(Debug)] @@ -86,7 +85,7 @@ impl<'r> FromRequest<'r> for PartialUserInfo { user_id: user_id as i32, created_at: user.created_on as i64, email: user.email, - hasTotp: at.is_some(), + has_totp: at.is_some(), }) } else { Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken)) @@ -109,7 +108,7 @@ impl<'r> FromRequest<'r> for TOTPAuthenticatedUserInfo { Outcome::Failure(e) => Outcome::Failure(e), Outcome::Forward(f) => Outcome::Forward(f), Outcome::Success(s) => { - if s.hasTotp { + if s.has_totp { Outcome::Success(Self { user_id: s.user_id, created_at: s.created_at, diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index 1dd8cd1..df92e6b 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -7,5 +7,6 @@ pub struct TFConfig { pub db_url: String, pub base: Url, pub magic_links_valid_for: i64, - pub session_tokens_valid_for: i64 + pub session_tokens_valid_for: i64, + pub totp_verification_valid_for: i64 } \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 17c28d7..18e86c1 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -33,12 +33,11 @@ async fn main() -> Result<(), Box> { std::process::exit(1); } - let config_file; - if Path::new("config.toml").exists() { - config_file = "config.toml".to_string(); + let config_file = if Path::new("config.toml").exists() { + "config.toml".to_string() } else { - config_file = std::env::var("CONFIG_FILE").unwrap(); - } + std::env::var("CONFIG_FILE").unwrap() + }; let config_data = match fs::read_to_string(&config_file) { Ok(d) => d, @@ -82,9 +81,7 @@ async fn main() -> Result<(), Box> { crate::routes::v1::auth::magic_link::magiclink_request, crate::routes::v1::signup::signup_request, crate::routes::v1::auth::verify_magic_link::verify_magic_link, - - - crate::routes::v1::auth::check_auth, + crate::routes::v1::totp_authenticators::totp_authenticators_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 aad0904..aa16fd9 100644 --- a/trifid-api/src/routes/v1/auth/mod.rs +++ b/trifid-api/src/routes/v1/auth/mod.rs @@ -1,4 +1,2 @@ -use crate::auth::PartialUserInfo; - pub mod verify_magic_link; pub mod magic_link; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index becd02a..3044952 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -1,2 +1,4 @@ pub mod auth; -pub mod signup; \ No newline at end of file +pub mod signup; + +pub mod totp_authenticators; \ 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 new file mode 100644 index 0000000..3d5b207 --- /dev/null +++ b/trifid-api/src/routes/v1/totp_authenticators.rs @@ -0,0 +1,50 @@ +use rocket::http::{ContentType, Status}; +use rocket::serde::json::Json; +use rocket::{State, post}; +use sqlx::PgPool; +use serde::{Serialize, Deserialize}; +use crate::auth::PartialUserInfo; +use crate::config::TFConfig; +use crate::tokens::create_totp_token; + +#[derive(Deserialize)] +pub struct TotpAuthenticatorsRequest {} +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpAuthenticatorsResponseMetadata {} +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpAuthenticatorsResponseData { + #[serde(rename = "totpToken")] + pub totp_token: String, + pub secret: String, + pub url: String, +} +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TotpAuthenticatorsResponse { + pub data: TotpAuthenticatorsResponseData, + pub metadata: TotpAuthenticatorsResponseMetadata, +} + +#[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 { + return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_EXISTS", "this user already has a totp authenticator on their account"))) + } + + // generate a totp token + let (totptoken, totpmachine) = match create_totp_token(user.email, db.inner(), config.inner()).await { + Ok(t) => t, + Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured issuing a totp token, try again later", e))) + }; + + Ok((ContentType::JSON, Json(TotpAuthenticatorsResponse { + data: TotpAuthenticatorsResponseData { + totp_token: totptoken, + secret: totpmachine.get_secret_base32(), + url: totpmachine.get_url(), + }, + metadata: TotpAuthenticatorsResponseMetadata {}, + }))) +} \ No newline at end of file diff --git a/trifid-api/src/tokens.rs b/trifid-api/src/tokens.rs index f7b0232..14cc243 100644 --- a/trifid-api/src/tokens.rs +++ b/trifid-api/src/tokens.rs @@ -5,10 +5,12 @@ use uuid::Uuid; use crate::config::TFConfig; use std::time::SystemTime; use std::time::UNIX_EPOCH; +use totp_rs::{Secret, TOTP}; +use crate::util::{TOTP_ALGORITHM, TOTP_DIGITS, TOTP_ISSUER, TOTP_SKEW, TOTP_STEP}; // https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com&token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box> { - let otp = format!("ml-{}", Uuid::new_v4().to_string()); + let otp = format!("ml-{}", Uuid::new_v4()); let otp_url = config.base.join(&format!("/auth/magic-link?email={}&token={}", urlencoding::encode(&email.clone()), otp.clone())).unwrap(); sqlx::query!("INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?; // TODO: send email @@ -25,7 +27,7 @@ pub async fn validate_session_token(token: String, db: &PgPool) -> Result $2", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64) } -pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool, config: &TFConfig) -> Result> { +pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool) -> Result> { let token = format!("at-{}", Uuid::new_v4()); sqlx::query!("INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, session_id, user_id as i32).execute(db).await?; Ok(token) @@ -34,4 +36,47 @@ pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) validate_session_token(token.clone(), db).await?; sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?; Ok(()) +} + + +/* +CREATE TABLE totp_create_tokens ( + id VARCHAR(39) NOT NULL PRIMARY KEY, + expires_on INTEGER NOT NULL, + totp_otpurl VARCHAR(3000) NOT NULL, + totp_secret VARCHAR(128) NOT NULL +); + */ + +pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) -> Result<(String, TOTP), Box> { + // create the TOTP parameters + + let secret = Secret::generate_secret(); + let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap(); + let otpurl = totpmachine.get_url(); + let otpsecret = totpmachine.get_secret_base32(); + + let otpid = format!("totp-{}", Uuid::new_v4()); + + sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?; + + Ok((otpid, totpmachine)) +} + +pub async fn verify_totp_token(otpid: String, email: String, db: &PgPool) -> Result> { + let totprow = sqlx::query!("SELECT * FROM totp_create_tokens WHERE id = $1 AND expires_on < $2", otpid, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?; + let secret = Secret::Encoded(totprow.totp_secret); + let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap(); + + if totpmachine.get_url() != totprow.totp_otpurl { + return Err("OTPURLs do not match (email does not match?)".into()) + } + + Ok(totpmachine) +} + +pub async fn use_totp_token(otpid: String, email: String, db: &PgPool) -> Result> { + let totpmachine = verify_totp_token(otpid.clone(), email, db).await?; + sqlx::query!("DELETE FROM totp_create_tokens WHERE id = $1", otpid).execute(db).await?; + Ok(totpmachine) } \ No newline at end of file diff --git a/trifid-api/src/util.rs b/trifid-api/src/util.rs index 1d66dc6..5ea8c3c 100644 --- a/trifid-api/src/util.rs +++ b/trifid-api/src/util.rs @@ -5,7 +5,7 @@ pub const TOTP_ALGORITHM: Algorithm = Algorithm::SHA1; pub const TOTP_DIGITS: usize = 6; pub const TOTP_SKEW: u8 = 1; pub const TOTP_STEP: u64 = 30; -pub const TOTP_ISSUER: &'static str = "trifidapi"; +pub const TOTP_ISSUER: &str = "trifidapi"; pub fn base64decode(val: &str) -> Result, base64::DecodeError> { base64::engine::general_purpose::STANDARD.decode(val)