use crate::ctx::Ctx;
use crate::feature::{Guard, GuardResult};
use crate::i18n::Text;
use crate::server::{Call, Interceptor};
#[cfg(feature = "oidc")]
pub(crate) mod jwt;
#[cfg(feature = "oidc")]
pub(crate) mod oidc;
#[cfg(any(feature = "oidc", feature = "totp"))]
pub(crate) mod seal;
#[cfg(feature = "totp")]
pub(crate) mod totp;
#[cfg(feature = "oidc")]
pub use jwt::{Algorithm, ClaimMap, JwtAuth};
#[cfg(feature = "oidc")]
pub use oidc::OidcAuth;
#[cfg(feature = "oidc")]
pub(crate) use oidc::{Flow, Session};
#[cfg(feature = "totp")]
pub use totp::{MemoryTotpStore, RECOVERY_CODES, Totp, TotpRecord, TotpStore};
pub(crate) const MFA_REQUIRED: &str = "zeyn.auth.mfa_required";
#[derive(Clone, Debug, Default)]
pub struct RequireMfa {
screens: Vec<&'static str>,
actions: Vec<&'static str>,
}
impl RequireMfa {
pub fn new() -> Self {
Self::default()
}
pub fn screen(mut self, name: &'static str) -> Self {
self.screens.push(name);
self
}
pub fn action(mut self, name: &'static str) -> Self {
self.actions.push(name);
self
}
fn covers(&self, call: &Call) -> bool {
(self.screens.is_empty() && self.actions.is_empty())
|| self.screens.contains(&call.screen)
|| call
.action
.as_ref()
.is_some_and(|a| self.actions.contains(&a.name))
}
}
impl Guard for RequireMfa {
fn check(&self, ctx: &Ctx) -> GuardResult {
if ctx.identity().mfa {
GuardResult::Allow
} else {
GuardResult::Deny(Text::key(MFA_REQUIRED))
}
}
}
impl Interceptor for RequireMfa {
fn name(&self) -> &'static str {
"RequireMfa"
}
fn before(&self, ctx: &Ctx, call: &Call) -> crate::Result<()> {
if !self.covers(call) || ctx.identity().mfa {
return Ok(());
}
Err(crate::Error::Denied(Text::key(MFA_REQUIRED)))
}
}
#[cfg(feature = "oidc")]
pub(crate) fn shows_mfa(amr: &[&str], acr: Option<&str>, mfa_acr: &[String]) -> bool {
const SECOND: &[&str] = &[
"mfa", "otp", "hwk", "swk", "sc", "sms", "fpt", "face", "iris", "retina", "vbm", "tel",
];
amr.iter().any(|m| SECOND.contains(m)) || acr.is_some_and(|a| mfa_acr.iter().any(|m| m == a))
}
#[cfg(any(feature = "oidc", feature = "totp"))]
pub(crate) fn safe_return(target: &str) -> Option<&str> {
let ok = target.starts_with('/')
&& !target.starts_with("//")
&& !target.contains('\\')
&& !target.chars().any(char::is_control)
&& target.len() <= 2048;
ok.then_some(target)
}
#[cfg(any(feature = "oidc", feature = "totp"))]
pub(crate) fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
#[cfg(any(feature = "oidc", feature = "totp"))]
pub(crate) fn random<const N: usize>() -> [u8; N] {
let mut bytes = [0u8; N];
getrandom::getrandom(&mut bytes).expect("the operating system random source must work");
bytes
}
#[cfg(any(feature = "oidc", feature = "totp"))]
pub(crate) fn b64url(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let n = (chunk[0] as u32) << 16
| (*chunk.get(1).unwrap_or(&0) as u32) << 8
| *chunk.get(2).unwrap_or(&0) as u32;
for i in 0..=chunk.len() {
out.push(ALPHABET[(n >> (18 - 6 * i) & 63) as usize] as char);
}
}
out
}
#[cfg(any(feature = "oidc", feature = "totp"))]
pub(crate) fn b64url_decode(s: &str) -> Option<Vec<u8>> {
let value = |c: u8| -> Option<u32> {
Some(match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'-' => 62,
b'_' => 63,
_ => return None,
} as u32)
};
if s.len() % 4 == 1 {
return None;
}
let mut out = Vec::with_capacity(s.len() * 3 / 4);
for chunk in s.as_bytes().chunks(4) {
let mut n = 0u32;
for (i, c) in chunk.iter().enumerate() {
n |= value(*c)? << (18 - 6 * i);
}
let bytes = [(n >> 16) as u8, (n >> 8) as u8, n as u8];
let keep = chunk.len() - 1;
if n & (0xFF_FFFF >> (8 * keep)) != 0 && keep < 3 {
return None;
}
out.extend_from_slice(&bytes[..keep]);
}
Some(out)
}