1use crate::{
4 authentication::credentials::{Credential, CredentialMetadata},
5 errors::{AuthError, Result},
6 tokens::AuthToken,
7};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub 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
21pub 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#[derive(Debug, Clone)]
35pub enum MethodResult {
36 Success(Box<AuthToken>),
38
39 MfaRequired(Box<MfaChallenge>),
41
42 Failure { reason: String },
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct MfaChallenge {
49 pub id: String,
51
52 pub mfa_type: MfaType,
54
55 pub user_id: String,
57
58 pub created_at: chrono::DateTime<chrono::Utc>,
60
61 pub expires_at: chrono::DateTime<chrono::Utc>,
63
64 pub attempts: u32,
66
67 pub max_attempts: u32,
69
70 pub code_hash: Option<String>,
73
74 pub message: Option<String>,
76
77 pub data: HashMap<String, serde_json::Value>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub enum MfaType {
84 Totp,
86
87 Sms { phone_number: String },
89
90 Email { email_address: String },
92
93 Push { device_id: String },
95
96 SecurityKey,
98
99 BackupCode,
101
102 MultiMethod,
104}
105
106pub trait AuthMethod: Send + Sync {
108 type MethodResult: Send + Sync + 'static;
109 type AuthToken: Send + Sync + 'static;
110
111 fn name(&self) -> &str;
113
114 fn authenticate(
116 &self,
117 credential: Credential,
118 metadata: CredentialMetadata,
119 ) -> impl std::future::Future<Output = Result<Self::MethodResult>> + Send;
120
121 fn validate_config(&self) -> Result<()>;
123
124 fn supports_refresh(&self) -> bool {
126 false
127 }
128
129 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
143pub type UserInfo = crate::auth::UserInfo;
145
146#[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#[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
186impl 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 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 pub fn id(&self) -> &str {
471 &self.id
472 }
473
474 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}