use super::AuthError;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use qrcode::QrCode;
use qrcode::render::svg;
use totp_rs::{Algorithm, Secret, TOTP};
pub struct TotpAuth {
issuer: String,
}
#[derive(Debug, Clone)]
pub struct TotpSetup {
pub secret: String,
pub qr_code_data_url: String,
pub otpauth_url: String,
}
impl TotpAuth {
pub fn new(issuer: &str) -> Self {
Self {
issuer: issuer.to_string(),
}
}
pub fn generate_secret(&self) -> String {
let secret = Secret::generate_secret();
secret.to_encoded().to_string()
}
pub fn setup(&self, user_email: &str) -> Result<TotpSetup, AuthError> {
let secret = self.generate_secret();
let totp = self.create_totp(&secret, user_email)?;
let otpauth_url = totp.get_url();
let qr_code_data_url = self.generate_qr_code(&otpauth_url)?;
Ok(TotpSetup {
secret,
qr_code_data_url,
otpauth_url,
})
}
pub fn verify(&self, secret: &str, code: &str, user_email: &str) -> Result<bool, AuthError> {
let totp = self.create_totp(secret, user_email)?;
Ok(totp.check_current(code).unwrap_or(false))
}
fn generate_qr_code(&self, data: &str) -> Result<String, AuthError> {
let code = QrCode::new(data.as_bytes())
.map_err(|e| AuthError::Internal(format!("QR code generation failed: {}", e)))?;
let svg_string = code
.render()
.min_dimensions(200, 200)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
let base64_svg = BASE64.encode(svg_string.as_bytes());
Ok(format!("data:image/svg+xml;base64,{}", base64_svg))
}
fn create_totp(&self, secret: &str, user_email: &str) -> Result<TOTP, AuthError> {
let secret = Secret::Encoded(secret.to_string());
TOTP::new(
Algorithm::SHA1,
6, 1, 30, secret
.to_bytes()
.map_err(|e| AuthError::Internal(format!("Invalid secret: {}", e)))?,
Some(self.issuer.clone()),
user_email.to_string(),
)
.map_err(|e| AuthError::Internal(format!("TOTP creation failed: {}", e)))
}
#[allow(dead_code)]
pub fn get_current_code(&self, secret: &str, user_email: &str) -> Result<String, AuthError> {
let totp = self.create_totp(secret, user_email)?;
totp.generate_current()
.map_err(|e| AuthError::Internal(format!("Code generation failed: {}", e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_secret() {
let totp = TotpAuth::new("AxonML");
let secret = totp.generate_secret();
assert!(!secret.is_empty());
assert!(secret.len() >= 16);
}
#[test]
fn test_setup() {
let totp = TotpAuth::new("AxonML");
let setup = totp.setup("test@example.com").unwrap();
assert!(!setup.secret.is_empty());
assert!(setup.secret.len() >= 16);
assert!(
setup
.qr_code_data_url
.starts_with("data:image/svg+xml;base64,")
);
assert!(setup.otpauth_url.contains("otpauth://totp/"));
assert!(setup.otpauth_url.contains("AxonML"));
}
#[test]
fn test_verify() {
let totp = TotpAuth::new("AxonML");
let setup = totp.setup("test@example.com").unwrap();
let current_code = totp
.get_current_code(&setup.secret, "test@example.com")
.unwrap();
let result = totp
.verify(&setup.secret, ¤t_code, "test@example.com")
.unwrap();
assert!(result);
let result = totp
.verify(&setup.secret, "000000", "test@example.com")
.unwrap();
assert!(!result);
}
}