committed by
GitHub
6 changed files with 295 additions and 138 deletions
@ -0,0 +1,175 @@ |
|||
use chrono::{TimeDelta, Utc}; |
|||
use serde::{de::DeserializeOwned, ser::Serialize}; |
|||
use std::sync::LazyLock; |
|||
|
|||
use crate::{ |
|||
CONFIG, |
|||
api::{ApiResult, EmptyResult}, |
|||
auth::{decode_jwt, encode_jwt}, |
|||
db::models::UserId, |
|||
}; |
|||
|
|||
static JWT_2FA_AUTH_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|api.2fa", CONFIG.domain_origin())); |
|||
|
|||
#[derive(Serialize, Deserialize)] |
|||
pub struct TwopFactorClaims<T> { |
|||
// Not before
|
|||
pub nbf: i64, |
|||
// Expiration time
|
|||
pub exp: i64, |
|||
// Issuer
|
|||
pub iss: String, |
|||
// Subject
|
|||
pub sub: UserId, |
|||
|
|||
pub enabled: bool, |
|||
|
|||
pub claims: T, |
|||
} |
|||
|
|||
#[derive(Serialize, Deserialize)] |
|||
pub struct AuthenticatorClaims { |
|||
pub key: String, |
|||
} |
|||
|
|||
#[derive(Serialize, Deserialize)] |
|||
pub struct DuoClaims { |
|||
data: Option<DuoData>, |
|||
} |
|||
|
|||
#[derive(Serialize, Deserialize, PartialEq)] |
|||
pub struct DuoData { |
|||
pub host: String, // Duo API hostname
|
|||
pub ik: String, // client id
|
|||
pub sk: String, // client secret
|
|||
} |
|||
|
|||
impl DuoData { |
|||
pub fn global() -> Option<Self> { |
|||
match (CONFIG._enable_duo(), CONFIG.duo_host()) { |
|||
(true, Some(host)) => Some(Self { |
|||
host, |
|||
ik: CONFIG.duo_ikey().unwrap(), |
|||
sk: CONFIG.duo_skey().unwrap(), |
|||
}), |
|||
_ => None, |
|||
} |
|||
} |
|||
pub fn msg(s: &str) -> Self { |
|||
Self { |
|||
host: s.into(), |
|||
ik: s.into(), |
|||
sk: s.into(), |
|||
} |
|||
} |
|||
pub fn secret() -> Self { |
|||
Self::msg("<global_secret>") |
|||
} |
|||
pub fn obscure(self) -> Self { |
|||
let mut host = self.host; |
|||
let mut ik = self.ik; |
|||
let mut sk = self.sk; |
|||
|
|||
let digits = 4; |
|||
let replaced = "************"; |
|||
|
|||
host.replace_range(digits.., replaced); |
|||
ik.replace_range(digits.., replaced); |
|||
sk.replace_range(digits.., replaced); |
|||
|
|||
Self { |
|||
host, |
|||
ik, |
|||
sk, |
|||
} |
|||
} |
|||
} |
|||
|
|||
#[derive(Serialize, Deserialize)] |
|||
pub struct EmailClaims { |
|||
pub email: Option<String>, |
|||
} |
|||
|
|||
fn token<T: Serialize>(user_id: UserId, enabled: bool, claims: T) -> String { |
|||
let time_now = Utc::now(); |
|||
let claims = TwopFactorClaims { |
|||
nbf: time_now.timestamp(), |
|||
exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(), |
|||
iss: JWT_2FA_AUTH_ISSUER.to_string(), |
|||
sub: user_id, |
|||
enabled, |
|||
claims, |
|||
}; |
|||
encode_jwt(&claims) |
|||
} |
|||
|
|||
fn validate<T: DeserializeOwned>(token: &str, user_id: &UserId, enabled: bool) -> ApiResult<T> { |
|||
match decode_jwt::<TwopFactorClaims<T>>(token, JWT_2FA_AUTH_ISSUER.to_string()) { |
|||
Ok(claims) => { |
|||
if claims.sub != *user_id { |
|||
err!("Invalid verification token: Invalid user"); |
|||
} |
|||
if claims.enabled != enabled { |
|||
err!("Invalid verification token: Invalid state"); |
|||
} |
|||
Ok(claims.claims) |
|||
} |
|||
Err(err) => err!(format!("Failed to decode verification token: {err}")), |
|||
} |
|||
} |
|||
|
|||
pub fn authenticator_token(user_id: UserId, key: String, enabled: bool) -> String { |
|||
token( |
|||
user_id, |
|||
enabled, |
|||
AuthenticatorClaims { |
|||
key, |
|||
}, |
|||
) |
|||
} |
|||
|
|||
pub fn validate_authenticator(token: &str, user_id: &UserId, key: &str, enabled: bool) -> EmptyResult { |
|||
let claims = validate::<AuthenticatorClaims>(token, user_id, enabled)?; |
|||
if claims.key != key { |
|||
err!("Invalid verification token: Invalid key"); |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
pub fn duo_token(user_id: UserId, data: Option<DuoData>, enabled: bool) -> String { |
|||
token( |
|||
user_id, |
|||
enabled, |
|||
DuoClaims { |
|||
data, |
|||
}, |
|||
) |
|||
} |
|||
|
|||
// When disabling we check that it's the correct data
|
|||
pub fn validate_duo(token: &str, user_id: &UserId, data: Option<&DuoData>, enabled: bool) -> EmptyResult { |
|||
let claims = validate::<DuoClaims>(token, user_id, enabled)?; |
|||
if enabled && claims.data.as_ref() != data { |
|||
err!("Invalid verification token: Invalid duo data"); |
|||
} |
|||
Ok(()) |
|||
} |
|||
|
|||
pub fn email_token(user_id: UserId, email: Option<String>, enabled: bool) -> String { |
|||
token( |
|||
user_id, |
|||
enabled, |
|||
EmailClaims { |
|||
email, |
|||
}, |
|||
) |
|||
} |
|||
|
|||
// When disabling we check that it's the correct `email`
|
|||
pub fn validate_email(token: &str, user_id: &UserId, email: String, enabled: bool) -> EmptyResult { |
|||
let claims = validate::<EmailClaims>(token, user_id, enabled)?; |
|||
if enabled && claims.email != Some(email) { |
|||
err!("Invalid verification token: Invalid email"); |
|||
} |
|||
Ok(()) |
|||
} |
|||
Loading…
Reference in new issue