mod admin;
mod impersonation;
mod password_reset;
mod signup;
use crate::authn::service::AuthnService;
use crate::authn::service::outcomes::FactorOutcome;
use crate::authn::{
error::AuthnError,
event::{AuthEventBuilder, AuthEventType},
factor::{FactorConfig, FactorKind},
store::{FactorStore, IdentityStore},
types::AuthnScope,
};
use crate::session::extractor::AuthSession;
impl<I, F> AuthnService<I, F>
where
I: IdentityStore,
F: FactorStore<Error = I::Error>,
{
pub(crate) async fn load_factor_with_fallback(
&self,
user_scope: &AuthnScope,
tenant_id: &crate::authn::ids::TenantId,
kind: FactorKind,
) -> Result<FactorConfig, AuthnError<I::Error>> {
if let Some(cfg) = self
.factors
.load_factor(user_scope, kind.clone())
.await
.map_err(AuthnError::Store)?
{
return Ok(cfg);
}
let tenant_scope = AuthnScope::Tenant(*tenant_id);
if let Some(cfg) = self
.factors
.load_factor(&tenant_scope, kind.clone())
.await
.map_err(AuthnError::Store)?
{
return Ok(cfg);
}
self.factors
.load_factor(&AuthnScope::Global, kind)
.await
.map_err(AuthnError::Store)?
.ok_or(AuthnError::NoFlow)
}
pub(crate) async fn complete_factor_step(
&self,
user_id: &crate::authn::ids::UserId,
tenant_id: &crate::authn::ids::TenantId,
session: &AuthSession,
) -> Result<FactorOutcome, AuthnError<I::Error>> {
let new_state = session.auth_state().await;
if new_state.is_authenticated() {
let factors_completed = match &new_state {
crate::session::data::AuthState::Authenticated {
factors_completed, ..
} => factors_completed.clone(),
_ => Vec::new(),
};
let sid = session.session_id().await;
if let Some(reg) = &self.registry {
if let Some(max) = self.max_sessions_per_user {
let active = reg.active_sessions(user_id).await;
if active.len() >= max {
let to_evict = active.len() - max + 1;
for old_sid in active.iter().take(to_evict) {
reg.invalidate_session(user_id, old_sid).await;
tracing::info!(
user_id = %user_id,
evicted_session = %old_sid,
"concurrent session limit reached; evicting oldest session"
);
self.metrics.session_invalidated();
}
}
}
if !reg.register(user_id, &sid).await {
tracing::error!(
user_id = %user_id,
"complete_factor_step register failed; clearing session and \
refusing authentication"
);
session.clear().await;
return Err(AuthnError::NoFlow);
}
match self.identity.account_status(user_id).await {
Ok(status) if !status.allows_login() => {
tracing::warn!(
user_id = %user_id,
status = ?status,
"account status flipped to non-loginable mid-flow; \
revoking just-registered session and refusing authentication"
);
reg.invalidate_session(user_id, &sid).await;
session.clear().await;
self.metrics.account_locked();
let until =
if let crate::authn::types::EntityState::Suspended(detail) = &status {
detail.until
} else {
None
};
return Ok(FactorOutcome::Locked { until });
}
Ok(_) => {}
Err(e) => {
tracing::warn!(
user_id = %user_id,
error = %e,
"post-register status re-check failed; failing closed"
);
reg.invalidate_session(user_id, &sid).await;
session.clear().await;
return Err(AuthnError::Store(e));
}
}
}
if let Err(e) = self.identity.reset_failed_attempts(user_id).await {
tracing::warn!(
user_id = %user_id,
error = %e,
"failed to reset failed-attempt counter post-auth; \
proceeding (counter will reset on next successful login)"
);
}
let mut event_builder = AuthEventBuilder::success(AuthEventType::Authenticated)
.attributed_to(user_id, tenant_id)
.with_session(sid);
for kind in &factors_completed {
event_builder = event_builder.with_factors_completed(kind.clone());
}
self.emit_audit(event_builder).await;
if let Err(e) = self
.identity
.record_last_login(user_id, self.clock.now())
.await
{
tracing::error!(error = %e, "failed to record last login");
}
self.metrics.auth_success();
return Ok(FactorOutcome::Authenticated);
}
let next_kind = match &new_state {
crate::session::data::AuthState::Authenticating { remaining, .. } => {
remaining.first().cloned()
}
_ => None,
};
Ok(next_kind.map_or(FactorOutcome::Authenticated, FactorOutcome::FactorRequired))
}
}