mssql_auth/
credentials.rs

1//! Credential types for authentication.
2//!
3//! This module provides credential types for various SQL Server authentication methods.
4//! When the `zeroize` feature is enabled, sensitive credential data is securely
5//! zeroed from memory when dropped.
6
7use std::borrow::Cow;
8
9#[cfg(feature = "zeroize")]
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12/// Credentials for SQL Server authentication.
13///
14/// This enum represents the various authentication methods supported.
15/// Credentials are designed to minimize copying of sensitive data.
16#[derive(Clone)]
17pub enum Credentials {
18    /// SQL Server authentication with username and password.
19    SqlServer {
20        /// Username.
21        username: Cow<'static, str>,
22        /// Password.
23        password: Cow<'static, str>,
24    },
25
26    /// Azure Active Directory / Entra ID access token.
27    AzureAccessToken {
28        /// The access token string.
29        token: Cow<'static, str>,
30    },
31
32    /// Azure Managed Identity (for VMs and containers).
33    #[cfg(feature = "azure-identity")]
34    AzureManagedIdentity {
35        /// Optional client ID for user-assigned identity.
36        client_id: Option<Cow<'static, str>>,
37    },
38
39    /// Azure Service Principal.
40    #[cfg(feature = "azure-identity")]
41    AzureServicePrincipal {
42        /// Tenant ID.
43        tenant_id: Cow<'static, str>,
44        /// Client ID.
45        client_id: Cow<'static, str>,
46        /// Client secret.
47        client_secret: Cow<'static, str>,
48    },
49
50    /// Integrated Windows Authentication (Kerberos/NTLM).
51    #[cfg(feature = "integrated-auth")]
52    Integrated,
53
54    /// Client certificate authentication.
55    #[cfg(feature = "cert-auth")]
56    Certificate {
57        /// Path to certificate file.
58        cert_path: Cow<'static, str>,
59        /// Optional password for encrypted certificates.
60        password: Option<Cow<'static, str>>,
61    },
62}
63
64impl Credentials {
65    /// Create SQL Server credentials.
66    pub fn sql_server(
67        username: impl Into<Cow<'static, str>>,
68        password: impl Into<Cow<'static, str>>,
69    ) -> Self {
70        Self::SqlServer {
71            username: username.into(),
72            password: password.into(),
73        }
74    }
75
76    /// Create Azure access token credentials.
77    pub fn azure_token(token: impl Into<Cow<'static, str>>) -> Self {
78        Self::AzureAccessToken {
79            token: token.into(),
80        }
81    }
82
83    /// Check if these credentials use SQL authentication.
84    #[must_use]
85    pub fn is_sql_auth(&self) -> bool {
86        matches!(self, Self::SqlServer { .. })
87    }
88
89    /// Check if these credentials use Azure AD.
90    #[must_use]
91    pub fn is_azure_ad(&self) -> bool {
92        #[allow(clippy::match_like_matches_macro)]
93        match self {
94            Self::AzureAccessToken { .. } => true,
95            #[cfg(feature = "azure-identity")]
96            Self::AzureManagedIdentity { .. } | Self::AzureServicePrincipal { .. } => true,
97            _ => false,
98        }
99    }
100
101    /// Get the authentication method name.
102    #[must_use]
103    pub fn method_name(&self) -> &'static str {
104        match self {
105            Self::SqlServer { .. } => "SQL Server Authentication",
106            Self::AzureAccessToken { .. } => "Azure AD Access Token",
107            #[cfg(feature = "azure-identity")]
108            Self::AzureManagedIdentity { .. } => "Azure Managed Identity",
109            #[cfg(feature = "azure-identity")]
110            Self::AzureServicePrincipal { .. } => "Azure Service Principal",
111            #[cfg(feature = "integrated-auth")]
112            Self::Integrated => "Integrated Authentication",
113            #[cfg(feature = "cert-auth")]
114            Self::Certificate { .. } => "Certificate Authentication",
115        }
116    }
117}
118
119impl std::fmt::Debug for Credentials {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        // Never expose sensitive data in debug output
122        match self {
123            Self::SqlServer { username, .. } => f
124                .debug_struct("SqlServer")
125                .field("username", username)
126                .field("password", &"[REDACTED]")
127                .finish(),
128            Self::AzureAccessToken { .. } => f
129                .debug_struct("AzureAccessToken")
130                .field("token", &"[REDACTED]")
131                .finish(),
132            #[cfg(feature = "azure-identity")]
133            Self::AzureManagedIdentity { client_id } => f
134                .debug_struct("AzureManagedIdentity")
135                .field("client_id", client_id)
136                .finish(),
137            #[cfg(feature = "azure-identity")]
138            Self::AzureServicePrincipal {
139                tenant_id,
140                client_id,
141                ..
142            } => f
143                .debug_struct("AzureServicePrincipal")
144                .field("tenant_id", tenant_id)
145                .field("client_id", client_id)
146                .field("client_secret", &"[REDACTED]")
147                .finish(),
148            #[cfg(feature = "integrated-auth")]
149            Self::Integrated => f.debug_struct("Integrated").finish(),
150            #[cfg(feature = "cert-auth")]
151            Self::Certificate { cert_path, .. } => f
152                .debug_struct("Certificate")
153                .field("cert_path", cert_path)
154                .field("password", &"[REDACTED]")
155                .finish(),
156        }
157    }
158}
159
160// =============================================================================
161// Secure Credentials (with zeroize feature)
162// =============================================================================
163
164/// A secret string that is securely zeroed from memory when dropped.
165///
166/// This type is only available when the `zeroize` feature is enabled.
167/// It ensures that sensitive data like passwords and tokens are overwritten
168/// with zeros when they go out of scope.
169#[cfg(feature = "zeroize")]
170#[derive(Clone, Zeroize, ZeroizeOnDrop)]
171pub struct SecretString(String);
172
173#[cfg(feature = "zeroize")]
174impl SecretString {
175    /// Create a new secret string.
176    pub fn new(value: impl Into<String>) -> Self {
177        Self(value.into())
178    }
179
180    /// Get the secret value.
181    ///
182    /// # Security
183    ///
184    /// Be careful with the returned reference - avoid logging or
185    /// copying the value unnecessarily.
186    #[must_use]
187    pub fn expose_secret(&self) -> &str {
188        &self.0
189    }
190}
191
192#[cfg(feature = "zeroize")]
193impl std::fmt::Debug for SecretString {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        write!(f, "[REDACTED]")
196    }
197}
198
199#[cfg(feature = "zeroize")]
200impl From<String> for SecretString {
201    fn from(s: String) -> Self {
202        Self::new(s)
203    }
204}
205
206#[cfg(feature = "zeroize")]
207impl From<&str> for SecretString {
208    fn from(s: &str) -> Self {
209        Self::new(s)
210    }
211}
212
213/// Secure credentials with automatic zeroization on drop.
214///
215/// This type is only available when the `zeroize` feature is enabled.
216/// All sensitive fields are securely zeroed from memory when the
217/// credentials are dropped.
218///
219/// # Example
220///
221/// ```rust,ignore
222/// use mssql_auth::SecureCredentials;
223///
224/// let creds = SecureCredentials::sql_server("user", "password");
225/// // When `creds` goes out of scope, the password is securely zeroed
226/// ```
227#[cfg(feature = "zeroize")]
228#[derive(Clone, Zeroize, ZeroizeOnDrop)]
229pub struct SecureCredentials {
230    kind: SecureCredentialKind,
231}
232
233#[cfg(feature = "zeroize")]
234#[derive(Clone, Zeroize, ZeroizeOnDrop)]
235enum SecureCredentialKind {
236    SqlServer {
237        username: String,
238        password: SecretString,
239    },
240    AzureAccessToken {
241        token: SecretString,
242    },
243    #[cfg(feature = "azure-identity")]
244    AzureManagedIdentity {
245        client_id: Option<String>,
246    },
247    #[cfg(feature = "azure-identity")]
248    AzureServicePrincipal {
249        tenant_id: String,
250        client_id: String,
251        client_secret: SecretString,
252    },
253    #[cfg(feature = "integrated-auth")]
254    Integrated,
255    #[cfg(feature = "cert-auth")]
256    Certificate {
257        cert_path: String,
258        password: Option<SecretString>,
259    },
260}
261
262#[cfg(feature = "zeroize")]
263impl SecureCredentials {
264    /// Create SQL Server credentials with secure password handling.
265    pub fn sql_server(username: impl Into<String>, password: impl Into<String>) -> Self {
266        Self {
267            kind: SecureCredentialKind::SqlServer {
268                username: username.into(),
269                password: SecretString::new(password),
270            },
271        }
272    }
273
274    /// Create Azure access token credentials with secure token handling.
275    pub fn azure_token(token: impl Into<String>) -> Self {
276        Self {
277            kind: SecureCredentialKind::AzureAccessToken {
278                token: SecretString::new(token),
279            },
280        }
281    }
282
283    /// Check if these credentials use SQL authentication.
284    #[must_use]
285    pub fn is_sql_auth(&self) -> bool {
286        matches!(self.kind, SecureCredentialKind::SqlServer { .. })
287    }
288
289    /// Check if these credentials use Azure AD.
290    #[must_use]
291    pub fn is_azure_ad(&self) -> bool {
292        #[allow(clippy::match_like_matches_macro)]
293        match &self.kind {
294            SecureCredentialKind::AzureAccessToken { .. } => true,
295            #[cfg(feature = "azure-identity")]
296            SecureCredentialKind::AzureManagedIdentity { .. }
297            | SecureCredentialKind::AzureServicePrincipal { .. } => true,
298            _ => false,
299        }
300    }
301
302    /// Get the authentication method name.
303    #[must_use]
304    pub fn method_name(&self) -> &'static str {
305        match &self.kind {
306            SecureCredentialKind::SqlServer { .. } => "SQL Server Authentication",
307            SecureCredentialKind::AzureAccessToken { .. } => "Azure AD Access Token",
308            #[cfg(feature = "azure-identity")]
309            SecureCredentialKind::AzureManagedIdentity { .. } => "Azure Managed Identity",
310            #[cfg(feature = "azure-identity")]
311            SecureCredentialKind::AzureServicePrincipal { .. } => "Azure Service Principal",
312            #[cfg(feature = "integrated-auth")]
313            SecureCredentialKind::Integrated => "Integrated Authentication",
314            #[cfg(feature = "cert-auth")]
315            SecureCredentialKind::Certificate { .. } => "Certificate Authentication",
316        }
317    }
318
319    /// Get the username for SQL Server authentication.
320    ///
321    /// Returns `None` for non-SQL authentication methods.
322    #[must_use]
323    pub fn username(&self) -> Option<&str> {
324        match &self.kind {
325            SecureCredentialKind::SqlServer { username, .. } => Some(username),
326            _ => None,
327        }
328    }
329
330    /// Get the password for SQL Server authentication.
331    ///
332    /// Returns `None` for non-SQL authentication methods.
333    ///
334    /// # Security
335    ///
336    /// Be careful with the returned reference - avoid logging or
337    /// copying the value unnecessarily.
338    #[must_use]
339    pub fn password(&self) -> Option<&str> {
340        match &self.kind {
341            SecureCredentialKind::SqlServer { password, .. } => Some(password.expose_secret()),
342            _ => None,
343        }
344    }
345
346    /// Get the token for Azure AD authentication.
347    ///
348    /// Returns `None` for non-Azure AD authentication methods.
349    ///
350    /// # Security
351    ///
352    /// Be careful with the returned reference - avoid logging or
353    /// copying the value unnecessarily.
354    #[must_use]
355    pub fn token(&self) -> Option<&str> {
356        match &self.kind {
357            SecureCredentialKind::AzureAccessToken { token } => Some(token.expose_secret()),
358            _ => None,
359        }
360    }
361}
362
363#[cfg(feature = "zeroize")]
364impl std::fmt::Debug for SecureCredentials {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        match &self.kind {
367            SecureCredentialKind::SqlServer { username, .. } => f
368                .debug_struct("SecureCredentials::SqlServer")
369                .field("username", username)
370                .field("password", &"[REDACTED]")
371                .finish(),
372            SecureCredentialKind::AzureAccessToken { .. } => f
373                .debug_struct("SecureCredentials::AzureAccessToken")
374                .field("token", &"[REDACTED]")
375                .finish(),
376            #[cfg(feature = "azure-identity")]
377            SecureCredentialKind::AzureManagedIdentity { client_id } => f
378                .debug_struct("SecureCredentials::AzureManagedIdentity")
379                .field("client_id", client_id)
380                .finish(),
381            #[cfg(feature = "azure-identity")]
382            SecureCredentialKind::AzureServicePrincipal {
383                tenant_id,
384                client_id,
385                ..
386            } => f
387                .debug_struct("SecureCredentials::AzureServicePrincipal")
388                .field("tenant_id", tenant_id)
389                .field("client_id", client_id)
390                .field("client_secret", &"[REDACTED]")
391                .finish(),
392            #[cfg(feature = "integrated-auth")]
393            SecureCredentialKind::Integrated => {
394                f.debug_struct("SecureCredentials::Integrated").finish()
395            }
396            #[cfg(feature = "cert-auth")]
397            SecureCredentialKind::Certificate { cert_path, .. } => f
398                .debug_struct("SecureCredentials::Certificate")
399                .field("cert_path", cert_path)
400                .field("password", &"[REDACTED]")
401                .finish(),
402        }
403    }
404}
405
406/// Convert from non-secure credentials to secure credentials.
407#[cfg(feature = "zeroize")]
408impl From<Credentials> for SecureCredentials {
409    fn from(creds: Credentials) -> Self {
410        match creds {
411            Credentials::SqlServer { username, password } => {
412                SecureCredentials::sql_server(username.into_owned(), password.into_owned())
413            }
414            Credentials::AzureAccessToken { token } => {
415                SecureCredentials::azure_token(token.into_owned())
416            }
417            #[cfg(feature = "azure-identity")]
418            Credentials::AzureManagedIdentity { client_id } => SecureCredentials {
419                kind: SecureCredentialKind::AzureManagedIdentity {
420                    client_id: client_id.map(|c| c.into_owned()),
421                },
422            },
423            #[cfg(feature = "azure-identity")]
424            Credentials::AzureServicePrincipal {
425                tenant_id,
426                client_id,
427                client_secret,
428            } => SecureCredentials {
429                kind: SecureCredentialKind::AzureServicePrincipal {
430                    tenant_id: tenant_id.into_owned(),
431                    client_id: client_id.into_owned(),
432                    client_secret: SecretString::new(client_secret.into_owned()),
433                },
434            },
435            #[cfg(feature = "integrated-auth")]
436            Credentials::Integrated => SecureCredentials {
437                kind: SecureCredentialKind::Integrated,
438            },
439            #[cfg(feature = "cert-auth")]
440            Credentials::Certificate {
441                cert_path,
442                password,
443            } => SecureCredentials {
444                kind: SecureCredentialKind::Certificate {
445                    cert_path: cert_path.into_owned(),
446                    password: password.map(|p| SecretString::new(p.into_owned())),
447                },
448            },
449        }
450    }
451}
452
453#[cfg(test)]
454#[allow(clippy::panic)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_credentials_sql_server() {
460        let creds = Credentials::sql_server("user", "password");
461        assert!(creds.is_sql_auth());
462        assert!(!creds.is_azure_ad());
463        match creds {
464            Credentials::SqlServer { username, password } => {
465                assert_eq!(username.as_ref(), "user");
466                assert_eq!(password.as_ref(), "password");
467            }
468            _ => panic!("Expected SqlServer variant"),
469        }
470    }
471
472    #[test]
473    fn test_credentials_azure_token() {
474        let creds = Credentials::azure_token("my-token");
475        assert!(!creds.is_sql_auth());
476        assert!(creds.is_azure_ad());
477        match creds {
478            Credentials::AzureAccessToken { token } => {
479                assert_eq!(token.as_ref(), "my-token");
480            }
481            _ => panic!("Expected AzureAccessToken variant"),
482        }
483    }
484
485    #[test]
486    fn test_credentials_debug_redacts_password() {
487        let creds = Credentials::sql_server("user", "supersecret");
488        let debug = format!("{:?}", creds);
489        assert!(debug.contains("user"));
490        assert!(!debug.contains("supersecret"));
491        assert!(debug.contains("REDACTED"));
492    }
493
494    #[test]
495    fn test_credentials_debug_redacts_token() {
496        let creds = Credentials::azure_token("supersecrettoken");
497        let debug = format!("{:?}", creds);
498        assert!(!debug.contains("supersecrettoken"));
499        assert!(debug.contains("REDACTED"));
500    }
501
502    #[cfg(feature = "zeroize")]
503    mod zeroize_tests {
504        use super::*;
505
506        #[test]
507        fn test_secret_string_creation() {
508            let secret = SecretString::new("my-password");
509            assert_eq!(secret.expose_secret(), "my-password");
510        }
511
512        #[test]
513        fn test_secret_string_from_string() {
514            let secret: SecretString = String::from("password").into();
515            assert_eq!(secret.expose_secret(), "password");
516        }
517
518        #[test]
519        fn test_secret_string_from_str() {
520            let secret: SecretString = "password".into();
521            assert_eq!(secret.expose_secret(), "password");
522        }
523
524        #[test]
525        fn test_secret_string_debug_redacted() {
526            let secret = SecretString::new("supersecret");
527            let debug = format!("{:?}", secret);
528            assert!(!debug.contains("supersecret"));
529            assert!(debug.contains("REDACTED"));
530        }
531
532        #[test]
533        fn test_secret_string_clone() {
534            let secret = SecretString::new("password");
535            let cloned = secret.clone();
536            assert_eq!(cloned.expose_secret(), "password");
537        }
538
539        #[test]
540        fn test_secure_credentials_sql_server() {
541            let creds = SecureCredentials::sql_server("user", "password");
542            assert_eq!(creds.username(), Some("user"));
543            assert_eq!(creds.password(), Some("password"));
544            assert!(creds.token().is_none());
545        }
546
547        #[test]
548        fn test_secure_credentials_azure_token() {
549            let creds = SecureCredentials::azure_token("my-token");
550            assert!(creds.username().is_none());
551            assert!(creds.password().is_none());
552            assert_eq!(creds.token(), Some("my-token"));
553        }
554
555        #[test]
556        fn test_secure_credentials_debug_redacts_password() {
557            let creds = SecureCredentials::sql_server("user", "supersecret");
558            let debug = format!("{:?}", creds);
559            assert!(debug.contains("user"));
560            assert!(!debug.contains("supersecret"));
561            assert!(debug.contains("REDACTED"));
562        }
563
564        #[test]
565        fn test_secure_credentials_debug_redacts_token() {
566            let creds = SecureCredentials::azure_token("supersecrettoken");
567            let debug = format!("{:?}", creds);
568            assert!(!debug.contains("supersecrettoken"));
569            assert!(debug.contains("REDACTED"));
570        }
571
572        #[test]
573        fn test_secure_credentials_from_credentials() {
574            let creds = Credentials::sql_server("user", "password");
575            let secure: SecureCredentials = creds.into();
576            assert_eq!(secure.username(), Some("user"));
577            assert_eq!(secure.password(), Some("password"));
578        }
579
580        #[test]
581        fn test_secure_credentials_clone() {
582            let creds = SecureCredentials::sql_server("user", "password");
583            let cloned = creds.clone();
584            assert_eq!(cloned.username(), Some("user"));
585            assert_eq!(cloned.password(), Some("password"));
586        }
587    }
588}