auth-framework 0.5.0-rc18

A comprehensive, production-ready authentication and authorization framework for Rust applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
//! Authentication method implementations.

use crate::{
    authentication::credentials::{Credential, CredentialMetadata},
    errors::{AuthError, Result},
    tokens::AuthToken,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// Import the specific auth method modules
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;

// Re-export types from submodules
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;

/// Result of an authentication attempt.
#[derive(Debug, Clone)]
pub enum MethodResult {
    /// Authentication was successful
    Success(Box<AuthToken>),

    /// Multi-factor authentication is required
    MfaRequired(Box<MfaChallenge>),

    /// Authentication failed
    Failure { reason: String },
}

/// Multi-factor authentication challenge.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaChallenge {
    /// Unique challenge ID
    pub id: String,

    /// Type of MFA required
    pub mfa_type: MfaType,

    /// User ID this challenge is for
    pub user_id: String,

    /// When the challenge was created
    pub created_at: chrono::DateTime<chrono::Utc>,

    /// When the challenge expires
    pub expires_at: chrono::DateTime<chrono::Utc>,

    /// Number of verification attempts made
    pub attempts: u32,

    /// Maximum allowed attempts before the challenge is invalidated
    pub max_attempts: u32,

    /// Hash of the expected OTP code (for Sms/Email/Totp methods).
    /// `None` for methods that verify externally (Push, SecurityKey).
    pub code_hash: Option<String>,

    /// Optional message or instructions to show the user
    pub message: Option<String>,

    /// Additional challenge data (e.g. masked phone, masked email, session token)
    pub data: HashMap<String, serde_json::Value>,
}

/// Types of multi-factor authentication.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MfaType {
    /// Time-based one-time password (TOTP)
    Totp,

    /// SMS verification code
    Sms { phone_number: String },

    /// Email verification code
    Email { email_address: String },

    /// Push notification
    Push { device_id: String },

    /// Hardware security key
    SecurityKey,

    /// Backup codes
    BackupCode,

    /// Cross-method challenge that requires satisfying multiple MFA methods simultaneously
    MultiMethod,
}

/// Trait for authentication methods.
pub trait AuthMethod: Send + Sync {
    type MethodResult: Send + Sync + 'static;
    type AuthToken: Send + Sync + 'static;

    /// Get the name of this authentication method.
    fn name(&self) -> &str;

    /// Authenticate using the provided credentials.
    fn authenticate(
        &self,
        credential: Credential,
        metadata: CredentialMetadata,
    ) -> impl std::future::Future<Output = Result<Self::MethodResult>> + Send;

    /// Validate configuration for this method.
    fn validate_config(&self) -> Result<()>;

    /// Check if this method supports refresh tokens.
    fn supports_refresh(&self) -> bool {
        false
    }

    /// Refresh a token if supported.
    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(),
            ))
        }
    }
}

/// Basic user information. Alias for the canonical [`crate::auth::UserInfo`].
pub type UserInfo = crate::auth::UserInfo;

/// Enum wrapper for all supported authentication methods (for registry)
#[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),
}

/// Simplified implementations - these would contain the full implementations
#[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;

// Add basic constructors for test compatibility
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 {
    /// Create a new MFA challenge.
    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(),
        }
    }

    /// Get the challenge ID.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Check if the challenge has expired.
    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
    }
}