Skip to main content

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