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 ip_source: crate::client_ip::Source,
pub user_agent: Option<String>,
pub request_id: Option<String>,
pub trace_id: Option<String>,
pub geo_country: Option<String>,
pub session_id: Option<String>,
}
impl<S> axum::extract::FromRequestParts<S> for AuditContext
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
let header = |name: &str| {
parts
.headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
};
let client_ip = parts
.extensions
.get::<crate::client_ip::ClientIp>()
.copied()
.unwrap_or_default();
let user_agent = header("user-agent");
#[cfg(feature = "request-id")]
let request_id = parts
.extensions
.get::<crate::middleware::request_id::RequestId>()
.map(|id| id.0.clone());
#[cfg(not(feature = "request-id"))]
let request_id = None;
#[cfg(feature = "trace-id")]
let trace_id = parts
.extensions
.get::<crate::middleware::trace_id::TraceContext>()
.map(|ctx| ctx.trace_id.clone());
#[cfg(not(feature = "trace-id"))]
let trace_id = None;
let session_id =
match crate::session::extractor::AuthSession::from_request_parts(parts, state).await {
Ok(session) => Some(session.session_id().await.to_string()),
Err(_) => None,
};
Ok(Self {
ip_address: client_ip.get(),
ip_source: client_ip.source(),
user_agent,
request_id,
trace_id,
geo_country: None,
session_id,
})
}
}
#[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, PartialEq, Eq)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum AuthFailureReason {
NotActive,
NoFactorsConfigured,
UnknownTenant,
InvalidTenantRow,
UnknownIdentifier,
CrossTenantImpersonation,
TokenRefresh,
TokenRefreshNoToken,
TokenRefreshUnknownProvider,
TokenRefreshProviderRejected,
CeremonyExpired,
CsrfMismatch,
MissingIssuer,
PkceVerifierInvalid,
TokenExchange,
ProviderMismatch,
Other(String),
}
impl AuthFailureReason {
pub fn as_str(&self) -> &str {
match self {
Self::NotActive => "not_active",
Self::NoFactorsConfigured => "no_factors_configured",
Self::UnknownTenant => "unknown_tenant",
Self::InvalidTenantRow => "invalid_tenant_row",
Self::UnknownIdentifier => "unknown_identifier",
Self::CrossTenantImpersonation => "cross_tenant_impersonation",
Self::TokenRefresh => "token_refresh",
Self::TokenRefreshNoToken => "token_refresh_no_token",
Self::TokenRefreshUnknownProvider => "token_refresh_unknown_provider",
Self::TokenRefreshProviderRejected => "token_refresh_provider_rejected",
Self::CeremonyExpired => "ceremony_expired",
Self::CsrfMismatch => "csrf_mismatch",
Self::MissingIssuer => "missing_issuer",
Self::PkceVerifierInvalid => "pkce_verifier_invalid",
Self::TokenExchange => "token_exchange",
Self::ProviderMismatch => "provider_mismatch",
Self::Other(s) => s,
}
}
}
impl From<&str> for AuthFailureReason {
fn from(s: &str) -> Self {
match s {
"not_active" => Self::NotActive,
"no_factors_configured" => Self::NoFactorsConfigured,
"unknown_tenant" => Self::UnknownTenant,
"invalid_tenant_row" => Self::InvalidTenantRow,
"unknown_identifier" => Self::UnknownIdentifier,
"cross_tenant_impersonation" => Self::CrossTenantImpersonation,
"token_refresh" => Self::TokenRefresh,
"token_refresh_no_token" => Self::TokenRefreshNoToken,
"token_refresh_unknown_provider" => Self::TokenRefreshUnknownProvider,
"token_refresh_provider_rejected" => Self::TokenRefreshProviderRejected,
"ceremony_expired" => Self::CeremonyExpired,
"csrf_mismatch" => Self::CsrfMismatch,
"missing_issuer" => Self::MissingIssuer,
"pkce_verifier_invalid" => Self::PkceVerifierInvalid,
"token_exchange" => Self::TokenExchange,
"provider_mismatch" => Self::ProviderMismatch,
other => Self::Other(other.to_owned()),
}
}
}
impl From<String> for AuthFailureReason {
fn from(s: String) -> Self {
match Self::from(s.as_str()) {
Self::Other(_) => Self::Other(s),
known => known,
}
}
}
impl FromStr for AuthFailureReason {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
impl fmt::Display for AuthFailureReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl Serialize for AuthFailureReason {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for AuthFailureReason {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::from(String::deserialize(deserializer)?))
}
}
#[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<std::net::IpAddr>,
pub ip_source: crate::client_ip::Source,
pub trace_id: Option<String>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
pub geo_country: Option<String>,
pub error: Option<AuthFailureReason>,
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;