Skip to main content

auth_framework/authentication/
credentials.rs

1//! Credential types for various authentication methods.
2
3use base64::Engine;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Represents different types of credentials that can be used for authentication.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", content = "data")]
10pub enum Credential {
11    /// Username and password credentials
12    Password { username: String, password: String },
13
14    /// OAuth authorization code flow
15    OAuth {
16        authorization_code: String,
17        redirect_uri: Option<String>,
18        code_verifier: Option<String>, // For PKCE
19        state: Option<String>,
20    },
21
22    /// OAuth refresh token
23    OAuthRefresh { refresh_token: String },
24
25    /// API key authentication
26    ApiKey { key: String },
27
28    /// JSON Web Token
29    Jwt { token: String },
30
31    /// Bearer token (generic)
32    Bearer { token: String },
33
34    /// Basic authentication (username:password base64 encoded)
35    Basic { credentials: String },
36
37    /// Custom authentication with flexible key-value pairs
38    Custom {
39        method: String,
40        data: HashMap<String, String>,
41    },
42
43    /// Multi-factor authentication token
44    Mfa {
45        primary_credential: Box<Credential>,
46        mfa_code: String,
47        challenge_id: String,
48    },
49
50    /// Certificate-based authentication
51    Certificate {
52        certificate: Vec<u8>,
53        private_key: Vec<u8>,
54        passphrase: Option<String>,
55    },
56
57    /// SAML assertion with secure XML signature validation
58    #[cfg(feature = "saml")]
59    Saml { assertion: String },
60
61    /// OpenID Connect ID token
62    OpenIdConnect {
63        id_token: String,
64        access_token: Option<String>,
65        refresh_token: Option<String>,
66    },
67
68    /// Device flow credentials (device code polling)
69    DeviceCode {
70        device_code: String,
71        client_id: String,
72        user_code: Option<String>,
73        verification_uri: Option<String>,
74        verification_uri_complete: Option<String>,
75        expires_in: Option<u64>,
76        interval: Option<u64>,
77    },
78
79    /// Enhanced device flow credentials with full OAuth device flow support
80    EnhancedDeviceFlow {
81        device_code: String,
82        user_code: String,
83        verification_uri: String,
84        verification_uri_complete: Option<String>,
85        expires_in: u64,
86        interval: u64,
87        client_id: String,
88        client_secret: Option<String>,
89        scopes: Vec<String>,
90    },
91
92    /// WebAuthn/FIDO2 Passkey authentication using pure Rust implementation
93    #[cfg(feature = "passkeys")]
94    Passkey {
95        credential_id: Vec<u8>,
96        assertion_response: String, // JSON-serialized AuthenticatorAssertionResponse
97    },
98}
99
100impl Credential {
101    /// Create password credentials.
102    ///
103    /// # Example
104    ///
105    /// ```rust
106    /// # use auth_framework::authentication::credentials::Credential;
107    /// let cred = Credential::password("alice", "s3cret!");
108    /// assert_eq!(cred.credential_type(), "password");
109    /// ```
110    pub fn password(username: impl Into<String>, password: impl Into<String>) -> Self {
111        Self::Password {
112            username: username.into(),
113            password: password.into(),
114        }
115    }
116
117    /// Create OAuth authorization code credentials.
118    ///
119    /// Start with the authorization code only; attach optional fields like
120    /// `redirect_uri` via the `OAuth` variant's public fields.
121    pub fn oauth_code(authorization_code: impl Into<String>) -> Self {
122        Self::OAuth {
123            authorization_code: authorization_code.into(),
124            redirect_uri: None,
125            code_verifier: None,
126            state: None,
127        }
128    }
129
130    /// Create OAuth authorization code credentials with PKCE.
131    ///
132    /// Includes a code verifier for the Proof Key for Code Exchange flow.
133    pub fn oauth_code_with_pkce(
134        authorization_code: impl Into<String>,
135        code_verifier: impl Into<String>,
136    ) -> Self {
137        Self::OAuth {
138            authorization_code: authorization_code.into(),
139            redirect_uri: None,
140            code_verifier: Some(code_verifier.into()),
141            state: None,
142        }
143    }
144
145    /// Create OAuth refresh token credentials
146    pub fn oauth_refresh(refresh_token: impl Into<String>) -> Self {
147        Self::OAuthRefresh {
148            refresh_token: refresh_token.into(),
149        }
150    }
151
152    /// Create device code credentials for device flow.
153    ///
154    /// Starts a device authorization grant with just the device code and
155    /// client ID.  Optional fields (`user_code`, `verification_uri`, etc.)
156    /// are available on the `DeviceCode` variant.
157    pub fn device_code(device_code: impl Into<String>, client_id: impl Into<String>) -> Self {
158        Self::DeviceCode {
159            device_code: device_code.into(),
160            client_id: client_id.into(),
161            user_code: None,
162            verification_uri: None,
163            verification_uri_complete: None,
164            expires_in: None,
165            interval: None,
166        }
167    }
168
169    /// Create API key credentials
170    pub fn api_key(key: impl Into<String>) -> Self {
171        Self::ApiKey { key: key.into() }
172    }
173
174    /// Create JWT credentials
175    pub fn jwt(token: impl Into<String>) -> Self {
176        Self::Jwt {
177            token: token.into(),
178        }
179    }
180
181    /// Create bearer token credentials
182    pub fn bearer(token: impl Into<String>) -> Self {
183        Self::Bearer {
184            token: token.into(),
185        }
186    }
187
188    /// Create basic authentication credentials
189    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
190        let credentials = format!("{}:{}", username.into(), password.into());
191        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials.as_bytes());
192
193        Self::Basic {
194            credentials: encoded,
195        }
196    }
197
198    /// Create custom credentials with arbitrary key-value data.
199    ///
200    /// The `method` string identifies the custom auth method, while
201    /// `data` carries method-specific fields.
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// # use auth_framework::authentication::credentials::Credential;
207    /// use std::collections::HashMap;
208    ///
209    /// let mut data = HashMap::new();
210    /// data.insert("hardware_token".into(), "ABC123".into());
211    /// let cred = Credential::custom("hardware", data);
212    /// ```
213    pub fn custom(method: impl Into<String>, data: HashMap<String, String>) -> Self {
214        Self::Custom {
215            method: method.into(),
216            data,
217        }
218    }
219
220    /// Create MFA credentials that wrap a primary credential with a
221    /// second-factor code and challenge identifier.
222    pub fn mfa(
223        primary_credential: Credential,
224        mfa_code: impl Into<String>,
225        challenge_id: impl Into<String>,
226    ) -> Self {
227        Self::Mfa {
228            primary_credential: Box::new(primary_credential),
229            mfa_code: mfa_code.into(),
230            challenge_id: challenge_id.into(),
231        }
232    }
233
234    /// Create certificate credentials from raw DER-encoded bytes.
235    ///
236    /// For certificates already verified by TLS, consider
237    /// [`client_cert_from_tls`](Self::client_cert_from_tls) instead.
238    pub fn certificate(
239        certificate: Vec<u8>,
240        private_key: Vec<u8>,
241        passphrase: Option<String>,
242    ) -> Self {
243        Self::Certificate {
244            certificate,
245            private_key,
246            passphrase,
247        }
248    }
249
250    /// Create credentials representing a client certificate that was already
251    /// verified by the mTLS handshake.  The private key is **not** required —
252    /// key possession was proved by TLS; supplying one has no effect.
253    pub fn client_cert_from_tls(der_certificate: Vec<u8>) -> Self {
254        Self::Certificate {
255            certificate: der_certificate,
256            private_key: vec![],
257            passphrase: None,
258        }
259    }
260
261    /// Create SAML assertion credentials - REMOVED for security
262    ///
263    /// Create OpenID Connect credentials
264    pub fn openid_connect(id_token: impl Into<String>) -> Self {
265        Self::OpenIdConnect {
266            id_token: id_token.into(),
267            access_token: None,
268            refresh_token: None,
269        }
270    }
271
272    /// Create passkey credentials (WebAuthn/FIDO2)
273    #[cfg(feature = "passkeys")]
274    pub fn passkey(credential_id: Vec<u8>, assertion_response: impl Into<String>) -> Self {
275        Self::Passkey {
276            credential_id,
277            assertion_response: assertion_response.into(),
278        }
279    }
280
281    /// Get the credential type as a string
282    pub fn credential_type(&self) -> &str {
283        match self {
284            Self::Password { .. } => "password",
285            Self::OAuth { .. } => "oauth",
286            Self::OAuthRefresh { .. } => "oauth_refresh",
287            Self::ApiKey { .. } => "api_key",
288            Self::Jwt { .. } => "jwt",
289            Self::Bearer { .. } => "bearer",
290            Self::Basic { .. } => "basic",
291            Self::Custom { method, .. } => method.as_str(),
292            Self::Mfa { .. } => "mfa",
293            Self::Certificate { .. } => "certificate",
294            #[cfg(feature = "saml")]
295            Self::Saml { .. } => "saml",
296            Self::OpenIdConnect { .. } => "openid_connect",
297            Self::DeviceCode { .. } => "device_code",
298            Self::EnhancedDeviceFlow { .. } => "enhanced_device_flow",
299            #[cfg(feature = "passkeys")]
300            Self::Passkey { .. } => "passkey",
301        }
302    }
303
304    /// Check if this credential supports refresh
305    pub fn supports_refresh(&self) -> bool {
306        matches!(
307            self,
308            Self::OAuth { .. } | Self::OAuthRefresh { .. } | Self::OpenIdConnect { .. }
309        )
310    }
311
312    /// Extract refresh token if available
313    pub fn refresh_token(&self) -> Option<&str> {
314        match self {
315            Self::OAuthRefresh { refresh_token } => Some(refresh_token),
316            Self::OpenIdConnect { refresh_token, .. } => refresh_token.as_deref(),
317            _ => None,
318        }
319    }
320
321    /// Check if this credential is sensitive and should be masked in logs
322    pub fn is_sensitive(&self) -> bool {
323        matches!(
324            self,
325            Self::Password { .. }
326                | Self::ApiKey { .. }
327                | Self::Jwt { .. }
328                | Self::Bearer { .. }
329                | Self::Basic { .. }
330                | Self::Certificate { .. }
331                | Self::Mfa { .. }
332        )
333    }
334
335    /// Get a safe representation for logging (masks sensitive data)
336    pub fn safe_display(&self) -> String {
337        match self {
338            Self::Password { username, .. } => {
339                format!("Password(username: {username})")
340            }
341            Self::OAuth { .. } => "OAuth(authorization_code)".to_string(),
342            Self::OAuthRefresh { .. } => "OAuthRefresh(refresh_token)".to_string(),
343            Self::ApiKey { .. } => "ApiKey(****)".to_string(),
344            Self::Jwt { .. } => "Jwt(****)".to_string(),
345            Self::Bearer { .. } => "Bearer(****)".to_string(),
346            Self::Basic { .. } => "Basic(****)".to_string(),
347            Self::Custom { method, .. } => format!("Custom(method: {method})"),
348            Self::Mfa { challenge_id, .. } => {
349                format!("Mfa(challenge_id: {challenge_id})")
350            }
351            Self::Certificate { .. } => "Certificate(****)".to_string(),
352            #[cfg(feature = "saml")]
353            Self::Saml { .. } => "Saml(****)".to_string(),
354            Self::OpenIdConnect { .. } => "OpenIdConnect(id_token)".to_string(),
355            Self::DeviceCode { .. } => "DeviceCode(****)".to_string(),
356            Self::EnhancedDeviceFlow { .. } => "EnhancedDeviceFlow(****)".to_string(),
357            #[cfg(feature = "passkeys")]
358            Self::Passkey { .. } => "Passkey(****)".to_string(),
359        }
360    }
361}
362
363/// Additional credential metadata that can be attached to any credential type.
364///
365/// Use the builder methods to construct metadata fluently:
366///
367/// ```rust
368/// # use auth_framework::authentication::credentials::CredentialMetadata;
369/// let meta = CredentialMetadata::new()
370///     .client_id("my-app")
371///     .client_ip("10.0.0.1")
372///     .scope("read")
373///     .scope("write");
374/// ```
375#[derive(Debug, Clone, Serialize, Deserialize, Default)]
376pub struct CredentialMetadata {
377    /// Client identifier (for OAuth flows).
378    pub client_id: Option<String>,
379
380    /// Requested scopes (e.g. `["read", "write"]`).
381    pub scopes: Vec<String>,
382
383    /// User-Agent string from the HTTP request, if available.
384    pub user_agent: Option<String>,
385
386    /// IP address of the authenticating client, if available.
387    pub client_ip: Option<String>,
388
389    /// Additional custom metadata key-value pairs.
390    pub custom: HashMap<String, String>,
391}
392
393impl CredentialMetadata {
394    /// Create empty credential metadata.
395    pub fn new() -> Self {
396        Self::default()
397    }
398
399    /// Set the OAuth client identifier.
400    pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
401        self.client_id = Some(client_id.into());
402        self
403    }
404
405    /// Append a single scope to the requested scopes.
406    pub fn scope(mut self, scope: impl Into<String>) -> Self {
407        self.scopes.push(scope.into());
408        self
409    }
410
411    /// Replace the requested scopes with the given list.
412    pub fn scopes(mut self, scopes: Vec<String>) -> Self {
413        self.scopes = scopes;
414        self
415    }
416
417    /// Set the User-Agent header value from the HTTP request.
418    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
419        self.user_agent = Some(user_agent.into());
420        self
421    }
422
423    /// Set the IP address of the authenticating client.
424    pub fn client_ip(mut self, client_ip: impl Into<String>) -> Self {
425        self.client_ip = Some(client_ip.into());
426        self
427    }
428
429    /// Add a custom metadata key-value pair.
430    pub fn custom(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
431        self.custom.insert(key.into(), value.into());
432        self
433    }
434}
435
436/// A complete authentication request with credentials and metadata.
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct AuthRequest {
439    /// The credentials to authenticate with
440    pub credential: Credential,
441
442    /// Additional metadata
443    pub metadata: CredentialMetadata,
444
445    /// Timestamp of the request
446    pub timestamp: chrono::DateTime<chrono::Utc>,
447}
448
449impl AuthRequest {
450    /// Create a new authentication request
451    pub fn new(credential: Credential) -> Self {
452        Self {
453            credential,
454            metadata: CredentialMetadata::default(),
455            timestamp: chrono::Utc::now(),
456        }
457    }
458
459    /// Create a new authentication request with metadata
460    pub fn with_metadata(credential: Credential, metadata: CredentialMetadata) -> Self {
461        Self {
462            credential,
463            metadata,
464            timestamp: chrono::Utc::now(),
465        }
466    }
467
468    /// Get a safe representation for logging
469    pub fn safe_display(&self) -> String {
470        format!(
471            "AuthRequest(credential: {}, client_id: {:?}, timestamp: {})",
472            self.credential.safe_display(),
473            self.metadata.client_id,
474            self.timestamp
475        )
476    }
477}