use crate::{
authn::{
factor::FactorKind,
ids::{DeviceId, TenantId, UserId},
},
session::id::SessionId,
};
use serde::{Deserialize, Serialize};
use std::{fmt, net::IpAddr, str::FromStr};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditContext {
pub ip_address: Option<IpAddr>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
pub geo_country: Option<String>,
pub session_id: Option<String>,
}
pub fn extract_audit_context(
headers: &axum::http::HeaderMap,
_session: Option<&crate::session::extractor::AuthSession>,
) -> AuditContext {
AuditContext {
ip_address: ip_from_headers(headers),
user_agent: headers
.get("user-agent")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string()),
request_id: headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string()),
geo_country: None,
session_id: None, }
}
pub async fn extract_audit_context_async(
headers: &axum::http::HeaderMap,
session: Option<&crate::session::extractor::AuthSession>,
) -> AuditContext {
let mut ctx = extract_audit_context(headers, session);
if let Some(s) = session {
ctx.session_id = Some(s.session_id().await.to_string());
}
ctx
}
pub fn ip_from_headers(headers: &axum::http::HeaderMap) -> Option<IpAddr> {
let raw = headers
.get("X-Real-IP")
.or_else(|| headers.get("X-Forwarded-For"))
.and_then(|v| v.to_str().ok())?;
raw.split(',').next().and_then(|s| s.trim().parse().ok())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum AuthEventType {
Authenticated,
LoginAttempt,
LogoutAttempt,
FactorVerified,
FactorSetup,
FactorEnabled,
FactorDisabled,
MethodEnabled,
MethodDisabled,
PasswordResetRequested,
PasswordReset,
SessionExpired,
SessionInvalidated,
SignupStarted,
SignupCompleted,
AccountSuspended,
AccountActivated,
Impersonation,
DeviceFirstSeen,
DeviceTrustGranted,
DeviceRevoked,
DevicePurged,
DeviceBindingAdded,
DeviceFingerprintMismatch,
}
impl AuthEventType {
pub fn as_str(&self) -> &'static str {
match self {
AuthEventType::Authenticated => "authenticated",
AuthEventType::LoginAttempt => "login_attempt",
AuthEventType::LogoutAttempt => "logout_attempt",
AuthEventType::FactorVerified => "factor_verified",
AuthEventType::FactorSetup => "factor_setup",
AuthEventType::FactorEnabled => "factor_enabled",
AuthEventType::FactorDisabled => "factor_disabled",
AuthEventType::MethodEnabled => "method_enabled",
AuthEventType::MethodDisabled => "method_disabled",
AuthEventType::PasswordResetRequested => "password_reset_requested",
AuthEventType::PasswordReset => "password_reset",
AuthEventType::SessionExpired => "session_expired",
AuthEventType::SessionInvalidated => "session_invalidated",
AuthEventType::SignupStarted => "signup_started",
AuthEventType::SignupCompleted => "signup_completed",
AuthEventType::AccountSuspended => "account_suspended",
AuthEventType::AccountActivated => "account_activated",
AuthEventType::Impersonation => "impersonation",
AuthEventType::DeviceFirstSeen => "device_first_seen",
AuthEventType::DeviceTrustGranted => "device_trust_granted",
AuthEventType::DeviceRevoked => "device_revoked",
AuthEventType::DevicePurged => "device_purged",
AuthEventType::DeviceBindingAdded => "device_binding_added",
AuthEventType::DeviceFingerprintMismatch => "device_fingerprint_mismatch",
}
}
}
impl FromStr for AuthEventType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"authenticated" => Ok(AuthEventType::Authenticated),
"login_attempt" => Ok(AuthEventType::LoginAttempt),
"logout_attempt" => Ok(AuthEventType::LogoutAttempt),
"factor_verified" => Ok(AuthEventType::FactorVerified),
"factor_setup" => Ok(AuthEventType::FactorSetup),
"factor_enabled" => Ok(AuthEventType::FactorEnabled),
"factor_disabled" => Ok(AuthEventType::FactorDisabled),
"method_enabled" => Ok(AuthEventType::MethodEnabled),
"method_disabled" => Ok(AuthEventType::MethodDisabled),
"password_reset_requested" => Ok(AuthEventType::PasswordResetRequested),
"password_reset" => Ok(AuthEventType::PasswordReset),
"session_expired" => Ok(AuthEventType::SessionExpired),
"session_invalidated" => Ok(AuthEventType::SessionInvalidated),
"signup_started" => Ok(AuthEventType::SignupStarted),
"signup_completed" => Ok(AuthEventType::SignupCompleted),
"account_suspended" => Ok(AuthEventType::AccountSuspended),
"account_activated" => Ok(AuthEventType::AccountActivated),
"impersonation" => Ok(AuthEventType::Impersonation),
"device_first_seen" => Ok(AuthEventType::DeviceFirstSeen),
"device_trust_granted" => Ok(AuthEventType::DeviceTrustGranted),
"device_revoked" => Ok(AuthEventType::DeviceRevoked),
"device_purged" => Ok(AuthEventType::DevicePurged),
"device_binding_added" => Ok(AuthEventType::DeviceBindingAdded),
"device_fingerprint_mismatch" => Ok(AuthEventType::DeviceFingerprintMismatch),
other => Err(format!("Unknown auth event type: {}", other)),
}
}
}
impl fmt::Display for AuthEventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum AuthEventStatus {
Success,
Failure,
Locked,
Expired,
Suspicious,
}
impl AuthEventStatus {
pub fn as_str(&self) -> &'static str {
match self {
AuthEventStatus::Success => "success",
AuthEventStatus::Failure => "failure",
AuthEventStatus::Locked => "locked",
AuthEventStatus::Expired => "expired",
AuthEventStatus::Suspicious => "suspicious",
}
}
}
impl FromStr for AuthEventStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"success" => Ok(AuthEventStatus::Success),
"failure" => Ok(AuthEventStatus::Failure),
"locked" => Ok(AuthEventStatus::Locked),
"expired" => Ok(AuthEventStatus::Expired),
"suspicious" => Ok(AuthEventStatus::Suspicious),
other => Err(format!("Unknown auth event status: {}", other)),
}
}
}
impl fmt::Display for AuthEventStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub struct AuthEvent {
pub user_id: Option<UserId>,
pub tenant_id: Option<TenantId>,
pub session_id: Option<SessionId>,
pub event_type: AuthEventType,
pub event_status: AuthEventStatus,
pub event_time: i64,
pub factor_kind: Option<FactorKind>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
pub geo_country: Option<String>,
pub error: Option<String>,
pub actor_id: Option<UserId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub device_id: Option<DeviceId>,
#[serde(default)]
pub factors_completed: Vec<FactorKind>,
}
mod builder;
pub use builder::AuthEventBuilder;
#[cfg(test)]
mod tests;