totp tokens
This commit is contained in:
parent
e79ee4c6c5
commit
6619662d0d
|
@ -3,3 +3,4 @@ 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
|
|
@ -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
|
||||||
|
);
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
|
@ -1,2 +1,4 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod signup;
|
pub mod signup;
|
||||||
|
|
||||||
|
pub mod totp_authenticators;
|
|
@ -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 {},
|
||||||
|
})))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
@ -35,3 +37,46 @@ pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool)
|
||||||
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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue