use crate::{
authentication::credentials::{Credential, CredentialMetadata},
errors::{AuthError, Result},
tokens::AuthToken,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub mod client_cert;
pub mod enhanced_device;
pub mod hardware_token;
#[cfg(feature = "ldap-auth")]
pub mod ldap;
pub mod passkey;
#[cfg(feature = "saml")]
pub mod saml;
pub use client_cert::ClientCertAuthMethod;
#[cfg(feature = "enhanced-device-flow")]
pub use enhanced_device::EnhancedDeviceFlowMethod;
pub use hardware_token::HardwareOtpToken;
#[cfg(feature = "ldap-auth")]
pub use ldap::{LdapAuthMethod, LdapConfig};
#[cfg(feature = "passkeys")]
pub use passkey::PasskeyAuthMethod;
#[cfg(feature = "saml")]
pub use saml::SamlAuthMethod;
#[derive(Debug, Clone)]
pub enum MethodResult {
Success(Box<AuthToken>),
MfaRequired(Box<MfaChallenge>),
Failure { reason: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaChallenge {
pub id: String,
pub mfa_type: MfaType,
pub user_id: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub attempts: u32,
pub max_attempts: u32,
pub code_hash: Option<String>,
pub message: Option<String>,
pub data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MfaType {
Totp,
Sms { phone_number: String },
Email { email_address: String },
Push { device_id: String },
SecurityKey,
BackupCode,
MultiMethod,
}
pub trait AuthMethod: Send + Sync {
type MethodResult: Send + Sync + 'static;
type AuthToken: Send + Sync + 'static;
fn name(&self) -> &str;
fn authenticate(
&self,
credential: Credential,
metadata: CredentialMetadata,
) -> impl std::future::Future<Output = Result<Self::MethodResult>> + Send;
fn validate_config(&self) -> Result<()>;
fn supports_refresh(&self) -> bool {
false
}
fn refresh_token(
&self,
_refresh_token: String,
) -> impl std::future::Future<Output = Result<AuthToken, AuthError>> + Send {
async {
Err(AuthError::auth_method(
self.name(),
"Token refresh not supported by this method".to_string(),
))
}
}
}
pub type UserInfo = crate::auth::UserInfo;
#[allow(clippy::large_enum_variant)]
pub enum AuthMethodEnum {
Password(PasswordMethod),
Jwt(JwtMethod),
ApiKey(ApiKeyMethod),
OAuth2(OAuth2Method),
#[cfg(feature = "saml")]
Saml(Box<SamlAuthMethod>),
#[cfg(feature = "ldap-auth")]
Ldap(LdapAuthMethod),
HardwareOtpToken(HardwareOtpToken),
ClientCert(ClientCertAuthMethod),
OpenIdConnect(OpenIdConnectAuthMethod),
AdvancedMfa(AdvancedMfaAuthMethod),
#[cfg(feature = "enhanced-device-flow")]
EnhancedDeviceFlow(Box<enhanced_device::EnhancedDeviceFlowMethod>),
#[cfg(feature = "passkeys")]
Passkey(PasskeyAuthMethod),
}
#[derive(Debug)]
pub struct PasswordMethod;
#[derive(Debug)]
pub struct JwtMethod;
#[derive(Debug)]
pub struct ApiKeyMethod;
#[derive(Debug)]
pub struct OAuth2Method;
#[derive(Debug)]
pub struct OpenIdConnectAuthMethod;
#[derive(Debug)]
pub struct AdvancedMfaAuthMethod;
impl Default for PasswordMethod {
fn default() -> Self {
Self::new()
}
}
impl PasswordMethod {
pub fn new() -> Self {
Self
}
}
impl Default for JwtMethod {
fn default() -> Self {
Self::new()
}
}
impl JwtMethod {
pub fn new() -> Self {
Self
}
pub fn secret_key(self, _secret: &str) -> Self {
self
}
pub fn issuer(self, _issuer: &str) -> Self {
self
}
pub fn audience(self, _audience: &str) -> Self {
self
}
}
impl Default for ApiKeyMethod {
fn default() -> Self {
Self::new()
}
}
impl ApiKeyMethod {
pub fn new() -> Self {
Self
}
}
impl Default for OAuth2Method {
fn default() -> Self {
Self::new()
}
}
impl OAuth2Method {
pub fn new() -> Self {
Self
}
}
impl AuthMethod for AuthMethodEnum {
type MethodResult = MethodResult;
type AuthToken = AuthToken;
fn name(&self) -> &str {
match self {
AuthMethodEnum::Password(_) => "password",
AuthMethodEnum::Jwt(_) => "jwt",
AuthMethodEnum::ApiKey(_) => "api_key",
AuthMethodEnum::OAuth2(_) => "oauth2",
#[cfg(feature = "saml")]
AuthMethodEnum::Saml(_) => "saml",
#[cfg(feature = "ldap-auth")]
AuthMethodEnum::Ldap(_) => "ldap",
AuthMethodEnum::HardwareOtpToken(_) => "hardware_token",
AuthMethodEnum::ClientCert(_) => "client_cert",
AuthMethodEnum::OpenIdConnect(_) => "openid_connect",
AuthMethodEnum::AdvancedMfa(_) => "advanced_mfa",
#[cfg(feature = "enhanced-device-flow")]
AuthMethodEnum::EnhancedDeviceFlow(_) => "enhanced_device_flow",
#[cfg(feature = "passkeys")]
AuthMethodEnum::Passkey(_) => "passkey",
}
}
async fn authenticate(
&self,
credential: Credential,
metadata: CredentialMetadata,
) -> Result<Self::MethodResult> {
let _ = &metadata;
match self {
#[cfg(feature = "saml")]
AuthMethodEnum::Saml(m) => return m.authenticate(credential, metadata).await,
#[cfg(feature = "passkeys")]
AuthMethodEnum::Passkey(m) => return m.authenticate(credential, metadata).await,
#[cfg(feature = "enhanced-device-flow")]
AuthMethodEnum::EnhancedDeviceFlow(m) => {
return m.authenticate(credential, metadata).await;
}
AuthMethodEnum::Password(_) => match credential {
Credential::Password { username, password } => {
if username.is_empty() || password.is_empty() {
return Self::failure("Username or password cannot be empty");
}
return Self::failure(
"Password authentication is handled by AuthFramework's built-in storage-backed password flow",
);
}
_ => {
return Self::failure("Password authentication expects Credential::password");
}
},
AuthMethodEnum::Jwt(_) => match credential {
Credential::Jwt { token } | Credential::Bearer { token } => {
if token.is_empty() {
return Self::failure("JWT token cannot be empty");
}
return Self::failure(
"JWT authentication must be performed through AuthFramework so the active TokenManager can validate the token signature",
);
}
_ => {
return Self::failure(
"JWT authentication expects Credential::jwt or Credential::bearer",
);
}
},
AuthMethodEnum::ApiKey(_) => match credential {
Credential::ApiKey { key } => {
if key.is_empty() {
return Self::failure("API key cannot be empty");
}
return Self::failure(
"API key authentication must be performed through AuthFramework so the stored key can be resolved to a user and session token",
);
}
_ => {
return Self::failure("API key authentication expects Credential::api_key");
}
},
AuthMethodEnum::OAuth2(_) => match credential {
Credential::OAuth {
authorization_code, ..
} => {
if authorization_code.is_empty() {
return Self::failure("OAuth authorization code cannot be empty");
}
return Self::failure(
"OAuth 2.0 authorization codes must be exchanged through an OAuth provider or server endpoint before authentication completes",
);
}
Credential::OAuthRefresh { refresh_token } => {
if refresh_token.is_empty() {
return Self::failure("OAuth refresh token cannot be empty");
}
return Self::failure(
"OAuth 2.0 refresh tokens must be exchanged through an OAuth provider or server endpoint before authentication completes",
);
}
Credential::Jwt { token }
| Credential::Bearer { token }
| Credential::OpenIdConnect {
id_token: token, ..
} => {
if token.is_empty() {
return Self::failure("OAuth token cannot be empty");
}
return Self::failure(
"OAuth 2.0 token authentication must be performed through AuthFramework so token validation and auditing use the active framework state",
);
}
_ => {
return Self::failure(
"OAuth2 authentication expects Credential::oauth_code, Credential::oauth_refresh, Credential::jwt, Credential::bearer, or Credential::openid_connect",
);
}
},
#[cfg(feature = "ldap-auth")]
AuthMethodEnum::Ldap(_) => {
return Self::failure(
"LDAP authentication requires a concrete LDAP integration and cannot use the generic AuthMethodEnum fallback",
);
}
AuthMethodEnum::HardwareOtpToken(_) => {
return Self::failure(
"Hardware token authentication requires the concrete hardware token flow rather than the generic AuthMethodEnum fallback",
);
}
AuthMethodEnum::ClientCert(_) => {
return Self::failure(
"Client certificate authentication requires the concrete client certificate flow rather than the generic AuthMethodEnum fallback",
);
}
AuthMethodEnum::OpenIdConnect(_) => {
return Self::failure(
"OpenID Connect authentication should be performed through the OIDC provider or AuthFramework integrations",
);
}
AuthMethodEnum::AdvancedMfa(_) => {
return Self::failure(
"Advanced MFA authentication requires the concrete MFA flow rather than the generic AuthMethodEnum fallback",
);
}
}
}
fn validate_config(&self) -> Result<()> {
match self {
AuthMethodEnum::Password(_) => Ok(()),
AuthMethodEnum::Jwt(_) => Ok(()),
AuthMethodEnum::ApiKey(_) => Ok(()),
AuthMethodEnum::OAuth2(_) => Ok(()),
#[cfg(feature = "saml")]
AuthMethodEnum::Saml(method) => method.validate_config(),
#[cfg(feature = "ldap-auth")]
AuthMethodEnum::Ldap(_) => Ok(()),
AuthMethodEnum::HardwareOtpToken(method) => {
if method.device_id.trim().is_empty() {
return Err(AuthError::config(
"Hardware token device_id cannot be empty",
));
}
if method.token_type.trim().is_empty() {
return Err(AuthError::config(
"Hardware token token_type cannot be empty",
));
}
Ok(())
}
AuthMethodEnum::ClientCert(_) => Ok(()),
AuthMethodEnum::OpenIdConnect(_) => Ok(()),
AuthMethodEnum::AdvancedMfa(_) => Ok(()),
#[cfg(feature = "enhanced-device-flow")]
AuthMethodEnum::EnhancedDeviceFlow(method) => method.validate_config(),
#[cfg(feature = "passkeys")]
AuthMethodEnum::Passkey(method) => method.validate_config(),
}
}
fn supports_refresh(&self) -> bool {
false
}
async fn refresh_token(&self, _refresh_token: String) -> Result<AuthToken, AuthError> {
Err(AuthError::auth_method(
self.name(),
"Token refresh not supported by this method".to_string(),
))
}
}
impl AuthMethodEnum {
fn failure(reason: impl Into<String>) -> Result<MethodResult> {
Ok(MethodResult::Failure {
reason: reason.into(),
})
}
}
impl MfaChallenge {
pub fn new(
mfa_type: MfaType,
user_id: impl Into<String>,
expires_in: std::time::Duration,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
mfa_type,
user_id: user_id.into(),
expires_at: chrono::Utc::now()
+ chrono::Duration::from_std(expires_in).unwrap_or(chrono::Duration::hours(1)),
created_at: chrono::Utc::now(),
attempts: 0,
max_attempts: 3,
code_hash: None,
message: None,
data: HashMap::new(),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now() > self.expires_at
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}