Skip to main content

auth_framework/methods/
mod.rs

1//! Authentication method implementations.
2
3use crate::{
4    authentication::credentials::{Credential, CredentialMetadata},
5    errors::{AuthError, Result},
6    tokens::AuthToken,
7};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11// Import the specific auth method modules
12pub mod client_cert;
13pub mod enhanced_device;
14pub mod hardware_token;
15#[cfg(feature = "ldap-auth")]
16pub mod ldap;
17pub mod passkey;
18#[cfg(feature = "saml")]
19pub mod saml;
20
21// Re-export types from submodules
22pub use client_cert::ClientCertAuthMethod;
23#[cfg(feature = "enhanced-device-flow")]
24pub use enhanced_device::EnhancedDeviceFlowMethod;
25pub use hardware_token::HardwareOtpToken;
26#[cfg(feature = "ldap-auth")]
27pub use ldap::{LdapAuthMethod, LdapConfig};
28#[cfg(feature = "passkeys")]
29pub use passkey::PasskeyAuthMethod;
30#[cfg(feature = "saml")]
31pub use saml::SamlAuthMethod;
32
33/// Result of an authentication attempt.
34#[derive(Debug, Clone)]
35pub enum MethodResult {
36    /// Authentication was successful
37    Success(Box<AuthToken>),
38
39    /// Multi-factor authentication is required
40    MfaRequired(Box<MfaChallenge>),
41
42    /// Authentication failed
43    Failure { reason: String },
44}
45
46/// Multi-factor authentication challenge.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct MfaChallenge {
49    /// Unique challenge ID
50    pub id: String,
51
52    /// Type of MFA required
53    pub mfa_type: MfaType,
54
55    /// User ID this challenge is for
56    pub user_id: String,
57
58    /// When the challenge was created
59    pub created_at: chrono::DateTime<chrono::Utc>,
60
61    /// When the challenge expires
62    pub expires_at: chrono::DateTime<chrono::Utc>,
63
64    /// Number of verification attempts made
65    pub attempts: u32,
66
67    /// Maximum allowed attempts before the challenge is invalidated
68    pub max_attempts: u32,
69
70    /// Hash of the expected OTP code (for Sms/Email/Totp methods).
71    /// `None` for methods that verify externally (Push, SecurityKey).
72    pub code_hash: Option<String>,
73
74    /// Optional message or instructions to show the user
75    pub message: Option<String>,
76
77    /// Additional challenge data (e.g. masked phone, masked email, session token)
78    pub data: HashMap<String, serde_json::Value>,
79}
80
81/// Types of multi-factor authentication.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub enum MfaType {
84    /// Time-based one-time password (TOTP)
85    Totp,
86
87    /// SMS verification code
88    Sms { phone_number: String },
89
90    /// Email verification code
91    Email { email_address: String },
92
93    /// Push notification
94    Push { device_id: String },
95
96    /// Hardware security key
97    SecurityKey,
98
99    /// Backup codes
100    BackupCode,
101
102    /// Cross-method challenge that requires satisfying multiple MFA methods simultaneously
103    MultiMethod,
104}
105
106/// Trait for authentication methods.
107pub trait AuthMethod: Send + Sync {
108    type MethodResult: Send + Sync + 'static;
109    type AuthToken: Send + Sync + 'static;
110
111    /// Get the name of this authentication method.
112    fn name(&self) -> &str;
113
114    /// Authenticate using the provided credentials.
115    fn authenticate(
116        &self,
117        credential: Credential,
118        metadata: CredentialMetadata,
119    ) -> impl std::future::Future<Output = Result<Self::MethodResult>> + Send;
120
121    /// Validate configuration for this method.
122    fn validate_config(&self) -> Result<()>;
123
124    /// Check if this method supports refresh tokens.
125    fn supports_refresh(&self) -> bool {
126        false
127    }
128
129    /// Refresh a token if supported.
130    fn refresh_token(
131        &self,
132        _refresh_token: String,
133    ) -> impl std::future::Future<Output = Result<AuthToken, AuthError>> + Send {
134        async {
135            Err(AuthError::auth_method(
136                self.name(),
137                "Token refresh not supported by this method".to_string(),
138            ))
139        }
140    }
141}
142
143/// Basic user information. Alias for the canonical [`crate::auth::UserInfo`].
144pub type UserInfo = crate::auth::UserInfo;
145
146/// Enum wrapper for all supported authentication methods (for registry)
147#[allow(clippy::large_enum_variant)]
148pub enum AuthMethodEnum {
149    Password(PasswordMethod),
150    Jwt(JwtMethod),
151    ApiKey(ApiKeyMethod),
152    OAuth2(OAuth2Method),
153    #[cfg(feature = "saml")]
154    Saml(Box<SamlAuthMethod>),
155    #[cfg(feature = "ldap-auth")]
156    Ldap(LdapAuthMethod),
157    HardwareOtpToken(HardwareOtpToken),
158    ClientCert(ClientCertAuthMethod),
159    OpenIdConnect(OpenIdConnectAuthMethod),
160    AdvancedMfa(AdvancedMfaAuthMethod),
161    #[cfg(feature = "enhanced-device-flow")]
162    EnhancedDeviceFlow(Box<enhanced_device::EnhancedDeviceFlowMethod>),
163    #[cfg(feature = "passkeys")]
164    Passkey(PasskeyAuthMethod),
165}
166
167/// Simplified implementations - these would contain the full implementations
168#[derive(Debug)]
169pub struct PasswordMethod;
170
171#[derive(Debug)]
172pub struct JwtMethod;
173
174#[derive(Debug)]
175pub struct ApiKeyMethod;
176
177#[derive(Debug)]
178pub struct OAuth2Method;
179
180#[derive(Debug)]
181pub struct OpenIdConnectAuthMethod;
182
183#[derive(Debug)]
184pub struct AdvancedMfaAuthMethod;
185
186// Add basic constructors for test compatibility
187impl Default for PasswordMethod {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193impl PasswordMethod {
194    pub fn new() -> Self {
195        Self
196    }
197}
198
199impl Default for JwtMethod {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl JwtMethod {
206    pub fn new() -> Self {
207        Self
208    }
209
210    pub fn secret_key(self, _secret: &str) -> Self {
211        self
212    }
213
214    pub fn issuer(self, _issuer: &str) -> Self {
215        self
216    }
217
218    pub fn audience(self, _audience: &str) -> Self {
219        self
220    }
221}
222
223impl Default for ApiKeyMethod {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229impl ApiKeyMethod {
230    pub fn new() -> Self {
231        Self
232    }
233}
234
235impl Default for OAuth2Method {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241impl OAuth2Method {
242    pub fn new() -> Self {
243        Self
244    }
245}
246
247impl AuthMethod for AuthMethodEnum {
248    type MethodResult = MethodResult;
249    type AuthToken = AuthToken;
250
251    fn name(&self) -> &str {
252        match self {
253            AuthMethodEnum::Password(_) => "password",
254            AuthMethodEnum::Jwt(_) => "jwt",
255            AuthMethodEnum::ApiKey(_) => "api_key",
256            AuthMethodEnum::OAuth2(_) => "oauth2",
257            #[cfg(feature = "saml")]
258            AuthMethodEnum::Saml(_) => "saml",
259            #[cfg(feature = "ldap-auth")]
260            AuthMethodEnum::Ldap(_) => "ldap",
261            AuthMethodEnum::HardwareOtpToken(_) => "hardware_token",
262            AuthMethodEnum::ClientCert(_) => "client_cert",
263            AuthMethodEnum::OpenIdConnect(_) => "openid_connect",
264            AuthMethodEnum::AdvancedMfa(_) => "advanced_mfa",
265            #[cfg(feature = "enhanced-device-flow")]
266            AuthMethodEnum::EnhancedDeviceFlow(_) => "enhanced_device_flow",
267            #[cfg(feature = "passkeys")]
268            AuthMethodEnum::Passkey(_) => "passkey",
269        }
270    }
271
272    async fn authenticate(
273        &self,
274        credential: Credential,
275        metadata: CredentialMetadata,
276    ) -> Result<Self::MethodResult> {
277        let _ = &metadata;
278        match self {
279            #[cfg(feature = "saml")]
280            AuthMethodEnum::Saml(m) => return m.authenticate(credential, metadata).await,
281            #[cfg(feature = "passkeys")]
282            AuthMethodEnum::Passkey(m) => return m.authenticate(credential, metadata).await,
283            #[cfg(feature = "enhanced-device-flow")]
284            AuthMethodEnum::EnhancedDeviceFlow(m) => {
285                return m.authenticate(credential, metadata).await;
286            }
287            AuthMethodEnum::Password(_) => match credential {
288                Credential::Password { username, password } => {
289                    if username.is_empty() || password.is_empty() {
290                        return Self::failure("Username or password cannot be empty");
291                    }
292                    return Self::failure(
293                        "Password authentication is handled by AuthFramework's built-in storage-backed password flow",
294                    );
295                }
296                _ => {
297                    return Self::failure("Password authentication expects Credential::password");
298                }
299            },
300            AuthMethodEnum::Jwt(_) => match credential {
301                Credential::Jwt { token } | Credential::Bearer { token } => {
302                    if token.is_empty() {
303                        return Self::failure("JWT token cannot be empty");
304                    }
305                    return Self::failure(
306                        "JWT authentication must be performed through AuthFramework so the active TokenManager can validate the token signature",
307                    );
308                }
309                _ => {
310                    return Self::failure(
311                        "JWT authentication expects Credential::jwt or Credential::bearer",
312                    );
313                }
314            },
315            AuthMethodEnum::ApiKey(_) => match credential {
316                Credential::ApiKey { key } => {
317                    if key.is_empty() {
318                        return Self::failure("API key cannot be empty");
319                    }
320                    return Self::failure(
321                        "API key authentication must be performed through AuthFramework so the stored key can be resolved to a user and session token",
322                    );
323                }
324                _ => {
325                    return Self::failure("API key authentication expects Credential::api_key");
326                }
327            },
328            AuthMethodEnum::OAuth2(_) => match credential {
329                Credential::OAuth {
330                    authorization_code, ..
331                } => {
332                    if authorization_code.is_empty() {
333                        return Self::failure("OAuth authorization code cannot be empty");
334                    }
335                    return Self::failure(
336                        "OAuth 2.0 authorization codes must be exchanged through an OAuth provider or server endpoint before authentication completes",
337                    );
338                }
339                Credential::OAuthRefresh { refresh_token } => {
340                    if refresh_token.is_empty() {
341                        return Self::failure("OAuth refresh token cannot be empty");
342                    }
343                    return Self::failure(
344                        "OAuth 2.0 refresh tokens must be exchanged through an OAuth provider or server endpoint before authentication completes",
345                    );
346                }
347                Credential::Jwt { token }
348                | Credential::Bearer { token }
349                | Credential::OpenIdConnect {
350                    id_token: token, ..
351                } => {
352                    if token.is_empty() {
353                        return Self::failure("OAuth token cannot be empty");
354                    }
355                    return Self::failure(
356                        "OAuth 2.0 token authentication must be performed through AuthFramework so token validation and auditing use the active framework state",
357                    );
358                }
359                _ => {
360                    return Self::failure(
361                        "OAuth2 authentication expects Credential::oauth_code, Credential::oauth_refresh, Credential::jwt, Credential::bearer, or Credential::openid_connect",
362                    );
363                }
364            },
365            #[cfg(feature = "ldap-auth")]
366            AuthMethodEnum::Ldap(_) => {
367                return Self::failure(
368                    "LDAP authentication requires a concrete LDAP integration and cannot use the generic AuthMethodEnum fallback",
369                );
370            }
371            AuthMethodEnum::HardwareOtpToken(_) => {
372                return Self::failure(
373                    "Hardware token authentication requires the concrete hardware token flow rather than the generic AuthMethodEnum fallback",
374                );
375            }
376            AuthMethodEnum::ClientCert(_) => {
377                return Self::failure(
378                    "Client certificate authentication requires the concrete client certificate flow rather than the generic AuthMethodEnum fallback",
379                );
380            }
381            AuthMethodEnum::OpenIdConnect(_) => {
382                return Self::failure(
383                    "OpenID Connect authentication should be performed through the OIDC provider or AuthFramework integrations",
384                );
385            }
386            AuthMethodEnum::AdvancedMfa(_) => {
387                return Self::failure(
388                    "Advanced MFA authentication requires the concrete MFA flow rather than the generic AuthMethodEnum fallback",
389                );
390            }
391        }
392    }
393
394    fn validate_config(&self) -> Result<()> {
395        match self {
396            AuthMethodEnum::Password(_) => Ok(()),
397            AuthMethodEnum::Jwt(_) => Ok(()),
398            AuthMethodEnum::ApiKey(_) => Ok(()),
399            AuthMethodEnum::OAuth2(_) => Ok(()),
400            #[cfg(feature = "saml")]
401            AuthMethodEnum::Saml(method) => method.validate_config(),
402            #[cfg(feature = "ldap-auth")]
403            AuthMethodEnum::Ldap(_) => Ok(()),
404            AuthMethodEnum::HardwareOtpToken(method) => {
405                if method.device_id.trim().is_empty() {
406                    return Err(AuthError::config(
407                        "Hardware token device_id cannot be empty",
408                    ));
409                }
410                if method.token_type.trim().is_empty() {
411                    return Err(AuthError::config(
412                        "Hardware token token_type cannot be empty",
413                    ));
414                }
415                Ok(())
416            }
417            AuthMethodEnum::ClientCert(_) => Ok(()),
418            AuthMethodEnum::OpenIdConnect(_) => Ok(()),
419            AuthMethodEnum::AdvancedMfa(_) => Ok(()),
420            #[cfg(feature = "enhanced-device-flow")]
421            AuthMethodEnum::EnhancedDeviceFlow(method) => method.validate_config(),
422            #[cfg(feature = "passkeys")]
423            AuthMethodEnum::Passkey(method) => method.validate_config(),
424        }
425    }
426
427    fn supports_refresh(&self) -> bool {
428        false
429    }
430
431    async fn refresh_token(&self, _refresh_token: String) -> Result<AuthToken, AuthError> {
432        Err(AuthError::auth_method(
433            self.name(),
434            "Token refresh not supported by this method".to_string(),
435        ))
436    }
437}
438
439impl AuthMethodEnum {
440    fn failure(reason: impl Into<String>) -> Result<MethodResult> {
441        Ok(MethodResult::Failure {
442            reason: reason.into(),
443        })
444    }
445}
446
447impl MfaChallenge {
448    /// Create a new MFA challenge.
449    pub fn new(
450        mfa_type: MfaType,
451        user_id: impl Into<String>,
452        expires_in: std::time::Duration,
453    ) -> Self {
454        Self {
455            id: uuid::Uuid::new_v4().to_string(),
456            mfa_type,
457            user_id: user_id.into(),
458            expires_at: chrono::Utc::now()
459                + chrono::Duration::from_std(expires_in).unwrap_or(chrono::Duration::hours(1)),
460            created_at: chrono::Utc::now(),
461            attempts: 0,
462            max_attempts: 3,
463            code_hash: None,
464            message: None,
465            data: HashMap::new(),
466        }
467    }
468
469    /// Get the challenge ID.
470    pub fn id(&self) -> &str {
471        &self.id
472    }
473
474    /// Check if the challenge has expired.
475    pub fn is_expired(&self) -> bool {
476        chrono::Utc::now() > self.expires_at
477    }
478
479    pub fn with_message(mut self, message: impl Into<String>) -> Self {
480        self.message = Some(message.into());
481        self
482    }
483}