109 lines
No EOL
6 KiB
Rust
109 lines
No EOL
6 KiB
Rust
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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<dyn Error>> {
|
|
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<String, Box<dyn Error>> {
|
|
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<i64, Box<dyn Error>> {
|
|
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<String, Box<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<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() + 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.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<TOTP, Box<dyn Error>> {
|
|
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<bool, Box<dyn Error>> {
|
|
Ok(sqlx::query!("SELECT totp_verified FROM users WHERE id = $1", user).fetch_one(db).await?.totp_verified == 1)
|
|
} |