totp tokens

This commit is contained in:
c0repwn3r 2023-02-07 10:37:25 -05:00
parent e79ee4c6c5
commit 6619662d0d
Signed by: core
GPG Key ID: FDBF740DADDCEECF
10 changed files with 120 additions and 21 deletions

View File

@ -2,4 +2,5 @@ listen_port = 8000
db_url = "postgres://postgres@localhost/trifidapi" db_url = "postgres://postgres@localhost/trifidapi"
base = "http://localhost:8000" base = "http://localhost:8000"
magic_links_valid_for = 86400 magic_links_valid_for = 86400
session_tokens_valid_for = 86400 session_tokens_valid_for = 86400
totp_verification_valid_for = 3600

View File

@ -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
);

View File

@ -1,14 +1,13 @@
use rocket::http::Status; use rocket::http::Status;
use rocket::{Request, State}; use rocket::{Request};
use rocket::request::{FromRequest, Outcome}; use rocket::request::{FromRequest, Outcome};
use sqlx::PgPool;
use crate::tokens::{validate_auth_token, validate_session_token}; use crate::tokens::{validate_auth_token, validate_session_token};
pub struct PartialUserInfo { pub struct PartialUserInfo {
pub user_id: i32, pub user_id: i32,
pub created_at: i64, pub created_at: i64,
pub email: String, pub email: String,
pub hasTotp: bool pub has_totp: bool
} }
#[derive(Debug)] #[derive(Debug)]
@ -86,7 +85,7 @@ impl<'r> FromRequest<'r> for PartialUserInfo {
user_id: user_id as i32, user_id: user_id as i32,
created_at: user.created_on as i64, created_at: user.created_on as i64,
email: user.email, email: user.email,
hasTotp: at.is_some(), has_totp: at.is_some(),
}) })
} else { } else {
Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken)) Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken))
@ -109,7 +108,7 @@ impl<'r> FromRequest<'r> for TOTPAuthenticatedUserInfo {
Outcome::Failure(e) => Outcome::Failure(e), Outcome::Failure(e) => Outcome::Failure(e),
Outcome::Forward(f) => Outcome::Forward(f), Outcome::Forward(f) => Outcome::Forward(f),
Outcome::Success(s) => { Outcome::Success(s) => {
if s.hasTotp { if s.has_totp {
Outcome::Success(Self { Outcome::Success(Self {
user_id: s.user_id, user_id: s.user_id,
created_at: s.created_at, created_at: s.created_at,

View File

@ -7,5 +7,6 @@ pub struct TFConfig {
pub db_url: String, pub db_url: String,
pub base: Url, pub base: Url,
pub magic_links_valid_for: i64, pub magic_links_valid_for: i64,
pub session_tokens_valid_for: i64 pub session_tokens_valid_for: i64,
pub totp_verification_valid_for: i64
} }

View File

@ -33,12 +33,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
std::process::exit(1); std::process::exit(1);
} }
let config_file; let config_file = if Path::new("config.toml").exists() {
if Path::new("config.toml").exists() { "config.toml".to_string()
config_file = "config.toml".to_string();
} else { } 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) { let config_data = match fs::read_to_string(&config_file) {
Ok(d) => d, Ok(d) => d,
@ -82,9 +81,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
crate::routes::v1::auth::magic_link::magiclink_request, crate::routes::v1::auth::magic_link::magiclink_request,
crate::routes::v1::signup::signup_request, crate::routes::v1::signup::signup_request,
crate::routes::v1::auth::verify_magic_link::verify_magic_link, crate::routes::v1::auth::verify_magic_link::verify_magic_link,
crate::routes::v1::totp_authenticators::totp_authenticators_request
crate::routes::v1::auth::check_auth,
]) ])
.register("/", catchers![ .register("/", catchers![
crate::routes::handler_400, crate::routes::handler_400,

View File

@ -1,4 +1,2 @@
use crate::auth::PartialUserInfo;
pub mod verify_magic_link; pub mod verify_magic_link;
pub mod magic_link; pub mod magic_link;

View File

@ -1,2 +1,4 @@
pub mod auth; pub mod auth;
pub mod signup; pub mod signup;
pub mod totp_authenticators;

View File

@ -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<TotpAuthenticatorsRequest>, user: PartialUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<TotpAuthenticatorsResponse>), (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 {},
})))
}

View File

@ -5,10 +5,12 @@ use uuid::Uuid;
use crate::config::TFConfig; use crate::config::TFConfig;
use std::time::SystemTime; use std::time::SystemTime;
use std::time::UNIX_EPOCH; 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 // 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<dyn Error>> { pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box<dyn Error>> {
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(); 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?; 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 // TODO: send email
@ -25,7 +27,7 @@ pub async fn validate_session_token(token: String, db: &PgPool) -> Result<i64, B
Ok(sqlx::query!("SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on > $2", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64) Ok(sqlx::query!("SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on > $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<String, Box<dyn Error>> { pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool) -> Result<String, Box<dyn Error>> {
let token = format!("at-{}", Uuid::new_v4()); 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?; 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) 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?; 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?; sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
Ok(()) 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<dyn Error>> {
// 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<TOTP, Box<dyn Error>> {
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<TOTP, Box<dyn Error>> {
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)
} }

View File

@ -5,7 +5,7 @@ pub const TOTP_ALGORITHM: Algorithm = Algorithm::SHA1;
pub const TOTP_DIGITS: usize = 6; pub const TOTP_DIGITS: usize = 6;
pub const TOTP_SKEW: u8 = 1; pub const TOTP_SKEW: u8 = 1;
pub const TOTP_STEP: u64 = 30; 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<Vec<u8>, base64::DecodeError> { pub fn base64decode(val: &str) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(val) base64::engine::general_purpose::STANDARD.decode(val)