// trifid-api, an open source reimplementation of the Defined Networking nebula management server. // Copyright (C) 2023 c0repwn3r // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::error::Error; use log::info; use sqlx::PgPool; 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()); let otp_url = config.web_root.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 info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for); Ok(()) } pub async fn generate_session_token(user_id: i64, db: &PgPool, config: &TFConfig) -> Result> { let token = format!("st-{}", Uuid::new_v4()); sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?; Ok(token) } pub async fn validate_session_token(token: String, db: &PgPool) -> Result> { 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) -> 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) } pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box> { 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(()) } /* 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() + 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.clone(), db).await?; 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) }