use super::model::User;
use super::{AccountEventKind, Accounts, UserManager, denied, now_secs, scope};
use crate::ctx::{Identity, Service};
use crate::security::RateLimit;
use crate::time::DateTime;
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
pub(crate) const SESSION: &str = "zeyn_account";
pub(crate) const TWO_FACTOR: &str = "zeyn_account_2fa";
pub(crate) const DEVICE: &str = "zeyn_account_device";
pub(crate) const BEARER: &str = "zeyn_account_bearer";
pub(crate) const REFRESH: &str = "zeyn_account_refresh";
const TWO_FACTOR_TTL: i64 = 300;
const MAX_COOKIE: usize = 3800;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub(crate) struct Session {
#[serde(rename = "i")]
pub id: String,
#[serde(rename = "u")]
pub user: String,
#[serde(rename = "r", default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<String>,
#[serde(rename = "p", default, skip_serializing_if = "Vec::is_empty")]
pub permissions: Vec<String>,
#[serde(rename = "t", default, skip_serializing_if = "Vec::is_empty")]
pub tenants: Vec<String>,
#[serde(rename = "s")]
pub stamp: String,
#[serde(rename = "a")]
pub auth_time: i64,
#[serde(rename = "m", default)]
pub mfa: bool,
#[serde(rename = "w", default)]
pub method: String,
#[serde(rename = "v")]
pub validated: i64,
#[serde(rename = "e")]
pub expires: i64,
#[serde(rename = "k", default)]
pub persistent: bool,
}
#[derive(Serialize, Deserialize)]
struct Pending {
#[serde(rename = "i")]
id: String,
#[serde(rename = "s")]
stamp: String,
#[serde(rename = "r")]
remember: bool,
#[serde(rename = "l", default)]
provider: Option<String>,
#[serde(rename = "e")]
expires: i64,
}
#[derive(Serialize, Deserialize)]
struct Device {
#[serde(rename = "i")]
id: String,
#[serde(rename = "s")]
stamp: String,
#[serde(rename = "e")]
expires: i64,
}
#[derive(Serialize, Deserialize)]
struct Refresh {
#[serde(rename = "i")]
id: String,
#[serde(rename = "s")]
stamp: String,
#[serde(rename = "w")]
method: String,
#[serde(rename = "m", default)]
mfa: bool,
#[serde(rename = "a")]
auth_time: i64,
#[serde(rename = "e")]
expires: i64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SignInResult {
Succeeded,
LockedOut,
NotAllowed,
RequiresTwoFactor,
Failed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TwoFactorKind {
Authenticator,
Email,
Phone,
}
#[derive(Clone, Serialize)]
pub struct AccessTokens {
#[serde(rename = "tokenType")]
pub token_type: &'static str,
#[serde(rename = "accessToken")]
pub access_token: String,
#[serde(rename = "expiresIn")]
pub expires_in: i64,
#[serde(rename = "refreshToken")]
pub refresh_token: String,
}
impl std::fmt::Debug for AccessTokens {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("AccessTokens([redacted])")
}
}
#[derive(Clone)]
pub struct SignInManager(pub(crate) Accounts);
impl Service for SignInManager {}
impl SignInManager {
fn users(&self) -> UserManager {
self.0.users()
}
fn sealer(&self) -> Result<&crate::auth::seal::Sealer> {
self.0.0.sealer.as_ref().ok_or_else(|| Error::Internal("accounts key too short".into()))
}
fn tag(&self, user: &User) -> String {
self.0.0.keys.stamp_tag(&user.id, &user.security_stamp)
}
pub fn can_sign_in(&self, user: &User) -> bool {
let o = self.0.0.options.sign_in;
!(o.require_confirmed_email && !user.email_confirmed)
&& !(o.require_confirmed_phone_number && !user.phone_number_confirmed)
}
async fn throttle(&self, account: &str) -> Result<()> {
let o = &self.0.0.options;
let ip = scope::ip().map(|i| i.to_string()).unwrap_or_else(|| "unknown".into());
let by_ip =
scope::limit(&format!("accounts:ip:{ip}"), RateLimit::per_ip(o.ip_attempts.0, o.ip_attempts.1)).await;
let by_account = scope::limit(
&format!("accounts:user:{}", super::normalize(account)),
RateLimit::per_user(o.account_attempts.0, o.account_attempts.1),
)
.await;
if by_ip.is_err() || by_account.is_err() {
self.0.event(AccountEventKind::RateLimited, None);
return Err(denied("rate_limited"));
}
Ok(())
}
pub(crate) async fn throttle_ip(&self) -> Result<()> {
let o = &self.0.0.options;
let ip = scope::ip().map(|i| i.to_string()).unwrap_or_else(|| "unknown".into());
if scope::limit(&format!("accounts:ip:{ip}"), RateLimit::per_ip(o.ip_attempts.0, o.ip_attempts.1))
.await
.is_err()
{
self.0.event(AccountEventKind::RateLimited, None);
return Err(denied("rate_limited"));
}
Ok(())
}
pub async fn password_sign_in(
&self,
name: &str,
password: &str,
remember: bool,
lockout_on_failure: bool,
) -> Result<SignInResult> {
self.throttle(name).await?;
let Some(mut user) = self.users().find_by_name_or_email(name).await? else {
self.users().dummy_check(password).await;
self.0.event(AccountEventKind::SignInFailed, None);
return Ok(SignInResult::Failed);
};
let result = self.check_password_sign_in(&mut user, password, lockout_on_failure).await?;
if result != SignInResult::Succeeded {
return Ok(result);
}
self.sign_in_or_two_factor(&user, remember, None).await
}
pub async fn check_password_sign_in(
&self,
user: &mut User,
password: &str,
lockout_on_failure: bool,
) -> Result<SignInResult> {
if let Some(early) = self.pre_sign_in(user) {
self.users().dummy_check(password).await;
return Ok(early);
}
if self.users().check_password(user, password).await? {
if !user.two_factor_enabled || self.is_two_factor_client_remembered(user) {
self.users().reset_access_failed(user).await?;
}
return Ok(SignInResult::Succeeded);
}
self.0.event(AccountEventKind::SignInFailed, Some(&user.id));
if lockout_on_failure && self.users().access_failed(user).await? {
return Ok(SignInResult::LockedOut);
}
Ok(SignInResult::Failed)
}
fn pre_sign_in(&self, user: &User) -> Option<SignInResult> {
if !self.can_sign_in(user) {
return Some(SignInResult::NotAllowed);
}
if user.is_locked_out(now_secs()) {
return Some(SignInResult::LockedOut);
}
None
}
pub async fn is_two_factor_enabled(&self, user: &User) -> Result<bool> {
Ok(user.two_factor_enabled && !self.users().two_factor_kinds(user).await?.is_empty())
}
async fn sign_in_or_two_factor(&self, user: &User, remember: bool, provider: Option<&str>) -> Result<SignInResult> {
if self.is_two_factor_enabled(user).await? && !self.is_two_factor_client_remembered(user) {
let pending = Pending {
id: user.id.clone(),
stamp: self.tag(user),
remember,
provider: provider.map(str::to_string),
expires: now_secs() + TWO_FACTOR_TTL,
};
scope::set_cookie(TWO_FACTOR, Some(&self.sealer()?.seal(TWO_FACTOR, &pending)), Some(TWO_FACTOR_TTL));
return Ok(SignInResult::RequiresTwoFactor);
}
self.sign_in(user, remember, provider.unwrap_or("pwd"), false).await?;
Ok(SignInResult::Succeeded)
}
pub(crate) async fn session(
&self,
user: &User,
persistent: bool,
method: &str,
mfa: bool,
auth_time: i64,
) -> Result<Session> {
let store = &self.0.0.store;
let roles = store.roles_of(&user.id).await?;
let claims = store.claims_of(&user.id).await?;
let tenants: Vec<String> = claims.iter().filter(|c| c.kind == "tenant").map(|c| c.value.clone()).collect();
let mut permissions: Vec<String> =
claims.into_iter().filter(|c| c.kind == "permission").map(|c| c.value).collect();
for role in &roles {
permissions.extend(
store.role_claims(&role.id).await?.into_iter().filter(|c| c.kind == "permission").map(|c| c.value),
);
}
permissions.sort();
permissions.dedup();
let now = now_secs();
Ok(Session {
id: user.id.clone(),
user: user.user_name.clone(),
roles: roles.into_iter().map(|r| r.name).collect(),
permissions,
tenants,
stamp: self.tag(user),
auth_time,
mfa,
method: method.to_string(),
validated: now,
expires: now + self.0.0.options.session.expire.as_secs() as i64,
persistent,
})
}
pub(crate) fn seal_session(&self, s: &Session) -> Result<String> {
let value = self.sealer()?.seal(SESSION, s);
if value.len() > MAX_COOKIE {
return Err(Error::Internal(format!(
"the account session cookie would be {} bytes; give the user fewer roles or permission claims",
value.len()
)));
}
Ok(value)
}
pub(crate) fn write_session(&self, s: &Session) -> Result<()> {
let value = self.seal_session(s)?;
let age = s.persistent.then(|| s.expires - now_secs());
scope::set_cookie(SESSION, Some(&value), age);
Ok(())
}
pub async fn sign_in(&self, user: &User, remember: bool, method: &str, mfa: bool) -> Result<()> {
let s = self.session(user, remember, method, mfa, now_secs()).await?;
self.write_session(&s)?;
scope::set_cookie(TWO_FACTOR, None, None);
self.0.event(AccountEventKind::SignedIn, Some(&user.id));
Ok(())
}
pub fn sign_out(&self, user: Option<&str>) {
scope::set_cookie(SESSION, None, None);
scope::set_cookie(TWO_FACTOR, None, None);
self.0.event(AccountEventKind::SignedOut, user);
}
pub(crate) fn current(&self) -> Option<Session> {
let value = scope::cookie(SESSION)?;
self.open_session(&value)
}
pub(crate) fn open_session(&self, value: &str) -> Option<Session> {
let s: Session = self.0.0.sealer.as_ref()?.open(SESSION, value)?;
(s.expires > now_secs()).then_some(s)
}
pub async fn refresh_sign_in(&self, user: &User) -> Result<()> {
let Some(old) = self.current() else { return Ok(()) };
if old.id != user.id {
return Ok(());
}
let s = self.session(user, old.persistent, &old.method, old.mfa, old.auth_time).await?;
self.write_session(&s)
}
pub async fn validate_security_stamp(&self, id: &str, tag: &str) -> Result<Option<User>> {
let Some(user) = self.users().find_by_id(id).await? else { return Ok(None) };
Ok(crate::server::same(&self.tag(&user), tag).then_some(user))
}
pub(crate) async fn revalidate(&self, s: &Session) -> Result<Option<Session>> {
let Some(user) = self.validate_security_stamp(&s.id, &s.stamp).await? else { return Ok(None) };
Ok(Some(self.session(&user, s.persistent, &s.method, s.mfa, s.auth_time).await?))
}
pub async fn two_factor_user(&self) -> Result<Option<User>> {
Ok(self.pending().await?.map(|(u, _)| u))
}
async fn pending(&self) -> Result<Option<(User, Pending)>> {
let Some(value) = scope::cookie(TWO_FACTOR) else { return Ok(None) };
let Some(p) = self.sealer()?.open::<Pending>(TWO_FACTOR, &value) else { return Ok(None) };
if p.expires <= now_secs() {
return Ok(None);
}
Ok(self.validate_security_stamp(&p.id, &p.stamp).await?.map(|u| (u, p)))
}
pub async fn send_two_factor_code(&self, ctx: &crate::Ctx, kind: TwoFactorKind) -> Result<()> {
self.throttle_ip().await?;
let Some((user, _)) = self.pending().await? else { return Err(denied("two_factor_expired")) };
let (to, code) = match kind {
TwoFactorKind::Email if user.email_confirmed => {
(user.email.clone(), self.users().generate_email_code(&user))
}
TwoFactorKind::Phone if user.phone_number_confirmed => {
(user.phone_number.clone(), self.users().generate_phone_code(&user))
}
_ => (None, String::new()),
};
let Some(to) = to else { return Err(denied("two_factor_unavailable")) };
self.0.0.sender.send(ctx, &to, super::AccountMessage::TwoFactorCode { code }).await
}
pub async fn two_factor_sign_in(
&self,
kind: TwoFactorKind,
code: &str,
remember_client: bool,
) -> Result<SignInResult> {
self.throttle_ip().await?;
let Some((mut user, pending)) = self.pending().await? else { return Ok(SignInResult::Failed) };
if let Some(early) = self.pre_sign_in(&user) {
return Ok(early);
}
let ok = match kind {
TwoFactorKind::Authenticator => self.users().verify_authenticator_code(&user, code).await?,
TwoFactorKind::Email => user.email_confirmed && self.users().verify_email_code(&user, code),
TwoFactorKind::Phone => user.phone_number_confirmed && self.users().verify_phone_code(&user, code),
};
if !ok {
self.0.event(AccountEventKind::SignInFailed, Some(&user.id));
if self.users().access_failed(&mut user).await? {
return Ok(SignInResult::LockedOut);
}
return Ok(SignInResult::Failed);
}
self.finish_two_factor(&mut user, &pending, remember_client).await
}
pub async fn recovery_code_sign_in(&self, code: &str) -> Result<SignInResult> {
self.throttle_ip().await?;
let Some((mut user, pending)) = self.pending().await? else { return Ok(SignInResult::Failed) };
if let Some(early) = self.pre_sign_in(&user) {
return Ok(early);
}
if !self.users().redeem_recovery_code(&user, code).await? {
self.0.event(AccountEventKind::SignInFailed, Some(&user.id));
if self.users().access_failed(&mut user).await? {
return Ok(SignInResult::LockedOut);
}
return Ok(SignInResult::Failed);
}
self.finish_two_factor(&mut user, &pending, false).await
}
async fn finish_two_factor(
&self,
user: &mut User,
pending: &Pending,
remember_client: bool,
) -> Result<SignInResult> {
self.users().reset_access_failed(user).await?;
if remember_client {
self.remember_two_factor_client(user)?;
}
let method = pending.provider.clone().unwrap_or_else(|| "mfa".into());
self.sign_in(user, pending.remember, &method, true).await?;
Ok(SignInResult::Succeeded)
}
pub fn is_two_factor_client_remembered(&self, user: &User) -> bool {
let Some(value) = scope::cookie(DEVICE) else { return false };
let Some(d) = self.0.0.sealer.as_ref().and_then(|s| s.open::<Device>(DEVICE, &value)) else { return false };
d.id == user.id && d.expires > now_secs() && crate::server::same(&d.stamp, &self.tag(user))
}
pub fn remember_two_factor_client(&self, user: &User) -> Result<()> {
let life = self.0.0.options.session.remember_two_factor.as_secs() as i64;
let d = Device { id: user.id.clone(), stamp: self.tag(user), expires: now_secs() + life };
scope::set_cookie(DEVICE, Some(&self.sealer()?.seal(DEVICE, &d)), Some(life));
Ok(())
}
pub fn forget_two_factor_client(&self) {
scope::set_cookie(DEVICE, None, None);
}
pub async fn external_login_sign_in(&self, provider: &str, key: &str, remember: bool) -> Result<SignInResult> {
let Some(user) = self.users().find_by_login(provider, key).await? else { return Ok(SignInResult::Failed) };
if let Some(early) = self.pre_sign_in(&user) {
return Ok(early);
}
self.sign_in_or_two_factor(&user, remember, Some(provider)).await
}
pub(crate) async fn passkey_sign_in(&self, user: &User, verified: bool, remember: bool) -> Result<SignInResult> {
if let Some(early) = self.pre_sign_in(user) {
return Ok(early);
}
self.sign_in(user, remember, "passkey", verified).await?;
Ok(SignInResult::Succeeded)
}
pub(crate) async fn bearer(&self, user: &User, method: &str, mfa: bool) -> Result<AccessTokens> {
self.issue(user, method, mfa, now_secs()).await
}
async fn issue(&self, user: &User, method: &str, mfa: bool, auth_time: i64) -> Result<AccessTokens> {
let o = self.0.0.options.session;
let now = now_secs();
let mut s = self.session(user, false, method, mfa, auth_time).await?;
s.expires = now + o.bearer_expiration.as_secs() as i64;
let refresh = Refresh {
id: user.id.clone(),
stamp: self.tag(user),
method: method.to_string(),
mfa,
auth_time,
expires: now + o.refresh_expiration.as_secs() as i64,
};
Ok(AccessTokens {
token_type: "Bearer",
access_token: self.sealer()?.seal(BEARER, &s),
expires_in: o.bearer_expiration.as_secs() as i64,
refresh_token: self.sealer()?.seal(REFRESH, &refresh),
})
}
pub(crate) async fn refresh_bearer(&self, token: &str) -> Result<Option<AccessTokens>> {
let Some(r) = self.sealer()?.open::<Refresh>(REFRESH, token) else { return Ok(None) };
if r.expires <= now_secs() {
return Ok(None);
}
let Some(user) = self.validate_security_stamp(&r.id, &r.stamp).await? else { return Ok(None) };
self.issue(&user, &r.method, r.mfa, r.auth_time).await.map(Some)
}
pub(crate) fn identity(&self, s: &Session) -> Identity {
let mut identity = Identity::user(s.user.clone());
identity.permissions.extend(s.permissions.iter().cloned());
identity.permissions.extend(self.0.0.roles.of(&s.roles).cloned());
identity.permissions.extend(self.0.0.everyone.iter().cloned());
identity.tenant = s.tenants.first().cloned();
identity.tenants.extend(s.tenants.iter().skip(1).cloned());
identity.authenticated_at = Some(DateTime::from_unix_ms(s.auth_time.saturating_mul(1000)));
identity.mfa = s.mfa;
identity
}
}
pub(crate) struct AccountsAuth(pub Accounts);
impl crate::server::Authenticator for AccountsAuth {
fn authenticate(&self, headers: &axum::http::HeaderMap) -> Option<Identity> {
let manager = self.0.sign_in();
if let Some(token) = bearer(headers) {
let s: Session = self.0.0.sealer.as_ref()?.open(BEARER, token)?;
return (s.expires > now_secs()).then(|| manager.identity(&s));
}
let s = manager.open_session(crate::server::cookie(headers, SESSION)?)?;
Some(manager.identity(&s))
}
}
pub(crate) fn bearer(headers: &axum::http::HeaderMap) -> Option<&str> {
let v = headers.get(axum::http::header::AUTHORIZATION)?.to_str().ok()?;
let (scheme, token) = v.split_once(' ')?;
scheme.eq_ignore_ascii_case("bearer").then_some(token.trim()).filter(|t| !t.is_empty() && t.len() <= 8192)
}