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    /// Azure default credential chain (managed identity, then the signed-in
52    /// `az` / `azd` CLI session). Maps to `Authentication=ActiveDirectoryDefault`.
53    #[cfg(feature = "azure-identity")]
54    AzureDefault,
55
56    /// Integrated Windows Authentication (Kerberos/NTLM).
57    #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
58    Integrated,
59
60    /// Client certificate authentication (Azure AD service principal with an
61    /// X.509 certificate).
62    ///
63    /// The certificate authenticates to Microsoft Entra, which issues the
64    /// access token used for the FEDAUTH login. This is NOT TDS-level mutual
65    /// TLS — SQL Server does not accept client certificates at the protocol
66    /// level.
67    #[cfg(feature = "cert-auth")]
68    Certificate {
69        /// Azure AD tenant ID.
70        tenant_id: Cow<'static, str>,
71        /// Application (client) ID of the service principal.
72        client_id: Cow<'static, str>,
73        /// Path to the certificate file: PKCS#12 (`.pfx`), or a PEM file
74        /// containing both the certificate and its private key.
75        cert_path: Cow<'static, str>,
76        /// Optional password protecting the certificate's private key.
77        password: Option<Cow<'static, str>>,
78    },
79}
80
81impl Credentials {
82    /// Create SQL Server credentials.
83    pub fn sql_server(
84        username: impl Into<Cow<'static, str>>,
85        password: impl Into<Cow<'static, str>>,
86    ) -> Self {
87        Self::SqlServer {
88            username: username.into(),
89            password: password.into(),
90        }
91    }
92
93    /// Create Azure access token credentials.
94    pub fn azure_token(token: impl Into<Cow<'static, str>>) -> Self {
95        Self::AzureAccessToken {
96            token: token.into(),
97        }
98    }
99
100    /// Create integrated authentication credentials (Windows SSPI or Kerberos/GSSAPI).
101    ///
102    /// Requires the `sspi-auth` (Windows) or `integrated-auth` (Linux/macOS) feature.
103    #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
104    #[must_use]
105    pub fn integrated() -> Self {
106        Self::Integrated
107    }
108
109    /// Create Azure default-credential-chain credentials
110    /// (`Authentication=ActiveDirectoryDefault`): managed identity, then the
111    /// signed-in `az` / `azd` CLI session.
112    ///
113    /// Requires the `azure-identity` feature.
114    #[cfg(feature = "azure-identity")]
115    #[must_use]
116    pub fn azure_default() -> Self {
117        Self::AzureDefault
118    }
119
120    /// Create client-certificate (Azure AD service principal) credentials.
121    ///
122    /// `cert_path` points at a PKCS#12 (`.pfx`) file, or a PEM file containing
123    /// both the certificate and its private key. The certificate authenticates
124    /// to Microsoft Entra, which issues the access token used for login.
125    ///
126    /// Requires the `cert-auth` feature.
127    #[cfg(feature = "cert-auth")]
128    pub fn certificate(
129        tenant_id: impl Into<Cow<'static, str>>,
130        client_id: impl Into<Cow<'static, str>>,
131        cert_path: impl Into<Cow<'static, str>>,
132        password: Option<Cow<'static, str>>,
133    ) -> Self {
134        Self::Certificate {
135            tenant_id: tenant_id.into(),
136            client_id: client_id.into(),
137            cert_path: cert_path.into(),
138            password,
139        }
140    }
141
142    /// Check if these credentials use SQL authentication.
143    #[must_use]
144    pub fn is_sql_auth(&self) -> bool {
145        matches!(self, Self::SqlServer { .. })
146    }
147
148    /// Check if these credentials use Azure AD.
149    #[must_use]
150    pub fn is_azure_ad(&self) -> bool {
151        #[allow(clippy::match_like_matches_macro)]
152        match self {
153            Self::AzureAccessToken { .. } => true,
154            #[cfg(feature = "azure-identity")]
155            Self::AzureManagedIdentity { .. }
156            | Self::AzureServicePrincipal { .. }
157            | Self::AzureDefault => true,
158            _ => false,
159        }
160    }
161
162    /// Get the authentication method name.
163    #[must_use]
164    pub fn method_name(&self) -> &'static str {
165        match self {
166            Self::SqlServer { .. } => "SQL Server Authentication",
167            Self::AzureAccessToken { .. } => "Azure AD Access Token",
168            #[cfg(feature = "azure-identity")]
169            Self::AzureManagedIdentity { .. } => "Azure Managed Identity",
170            #[cfg(feature = "azure-identity")]
171            Self::AzureServicePrincipal { .. } => "Azure Service Principal",
172            #[cfg(feature = "azure-identity")]
173            Self::AzureDefault => "Azure Default Credential Chain",
174            #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
175            Self::Integrated => "Integrated Authentication",
176            #[cfg(feature = "cert-auth")]
177            Self::Certificate { .. } => "Certificate Authentication",
178        }
179    }
180}
181
182impl std::fmt::Debug for Credentials {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        // Never expose sensitive data in debug output
185        match self {
186            Self::SqlServer { username, .. } => f
187                .debug_struct("SqlServer")
188                .field("username", username)
189                .field("password", &"[REDACTED]")
190                .finish(),
191            Self::AzureAccessToken { .. } => f
192                .debug_struct("AzureAccessToken")
193                .field("token", &"[REDACTED]")
194                .finish(),
195            #[cfg(feature = "azure-identity")]
196            Self::AzureManagedIdentity { client_id } => f
197                .debug_struct("AzureManagedIdentity")
198                .field("client_id", client_id)
199                .finish(),
200            #[cfg(feature = "azure-identity")]
201            Self::AzureServicePrincipal {
202                tenant_id,
203                client_id,
204                ..
205            } => f
206                .debug_struct("AzureServicePrincipal")
207                .field("tenant_id", tenant_id)
208                .field("client_id", client_id)
209                .field("client_secret", &"[REDACTED]")
210                .finish(),
211            #[cfg(feature = "azure-identity")]
212            Self::AzureDefault => f.debug_struct("AzureDefault").finish(),
213            #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
214            Self::Integrated => f.debug_struct("Integrated").finish(),
215            #[cfg(feature = "cert-auth")]
216            Self::Certificate {
217                tenant_id,
218                client_id,
219                cert_path,
220                ..
221            } => f
222                .debug_struct("Certificate")
223                .field("tenant_id", tenant_id)
224                .field("client_id", client_id)
225                .field("cert_path", cert_path)
226                .field("password", &"[REDACTED]")
227                .finish(),
228        }
229    }
230}
231
232// =============================================================================
233// Secure Credentials (with zeroize feature)
234// =============================================================================
235
236/// A secret string that is securely zeroed from memory when dropped.
237///
238/// This type is only available when the `zeroize` feature is enabled.
239/// It ensures that sensitive data like passwords and tokens are overwritten
240/// with zeros when they go out of scope.
241#[cfg(feature = "zeroize")]
242#[derive(Clone, Zeroize, ZeroizeOnDrop)]
243pub struct SecretString(String);
244
245#[cfg(feature = "zeroize")]
246impl SecretString {
247    /// Create a new secret string.
248    pub fn new(value: impl Into<String>) -> Self {
249        Self(value.into())
250    }
251
252    /// Get the secret value.
253    ///
254    /// # Security
255    ///
256    /// Be careful with the returned reference - avoid logging or
257    /// copying the value unnecessarily.
258    #[must_use]
259    pub fn expose_secret(&self) -> &str {
260        &self.0
261    }
262}
263
264#[cfg(feature = "zeroize")]
265impl std::fmt::Debug for SecretString {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        write!(f, "[REDACTED]")
268    }
269}
270
271#[cfg(feature = "zeroize")]
272impl From<String> for SecretString {
273    fn from(s: String) -> Self {
274        Self::new(s)
275    }
276}
277
278#[cfg(feature = "zeroize")]
279impl From<&str> for SecretString {
280    fn from(s: &str) -> Self {
281        Self::new(s)
282    }
283}
284
285/// Secure credentials with automatic zeroization on drop.
286///
287/// This type is only available when the `zeroize` feature is enabled.
288/// All sensitive fields are securely zeroed from memory when the
289/// credentials are dropped.
290///
291/// # Example
292///
293/// ```rust,ignore
294/// use mssql_auth::SecureCredentials;
295///
296/// let creds = SecureCredentials::sql_server("user", "password");
297/// // When `creds` goes out of scope, the password is securely zeroed
298/// ```
299#[cfg(feature = "zeroize")]
300#[derive(Clone, Zeroize, ZeroizeOnDrop)]
301pub struct SecureCredentials {
302    kind: SecureCredentialKind,
303}
304
305#[cfg(feature = "zeroize")]
306#[derive(Clone, Zeroize, ZeroizeOnDrop)]
307enum SecureCredentialKind {
308    SqlServer {
309        username: String,
310        password: SecretString,
311    },
312    AzureAccessToken {
313        token: SecretString,
314    },
315    #[cfg(feature = "azure-identity")]
316    AzureManagedIdentity {
317        client_id: Option<String>,
318    },
319    #[cfg(feature = "azure-identity")]
320    AzureServicePrincipal {
321        tenant_id: String,
322        client_id: String,
323        client_secret: SecretString,
324    },
325    #[cfg(feature = "azure-identity")]
326    AzureDefault,
327    #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
328    Integrated,
329    #[cfg(feature = "cert-auth")]
330    Certificate {
331        tenant_id: String,
332        client_id: String,
333        cert_path: String,
334        password: Option<SecretString>,
335    },
336}
337
338#[cfg(feature = "zeroize")]
339impl SecureCredentials {
340    /// Create SQL Server credentials with secure password handling.
341    pub fn sql_server(username: impl Into<String>, password: impl Into<String>) -> Self {
342        Self {
343            kind: SecureCredentialKind::SqlServer {
344                username: username.into(),
345                password: SecretString::new(password),
346            },
347        }
348    }
349
350    /// Create Azure access token credentials with secure token handling.
351    pub fn azure_token(token: impl Into<String>) -> Self {
352        Self {
353            kind: SecureCredentialKind::AzureAccessToken {
354                token: SecretString::new(token),
355            },
356        }
357    }
358
359    /// Check if these credentials use SQL authentication.
360    #[must_use]
361    pub fn is_sql_auth(&self) -> bool {
362        matches!(self.kind, SecureCredentialKind::SqlServer { .. })
363    }
364
365    /// Check if these credentials use Azure AD.
366    #[must_use]
367    pub fn is_azure_ad(&self) -> bool {
368        #[allow(clippy::match_like_matches_macro)]
369        match &self.kind {
370            SecureCredentialKind::AzureAccessToken { .. } => true,
371            #[cfg(feature = "azure-identity")]
372            SecureCredentialKind::AzureManagedIdentity { .. }
373            | SecureCredentialKind::AzureServicePrincipal { .. } => true,
374            _ => false,
375        }
376    }
377
378    /// Get the authentication method name.
379    #[must_use]
380    pub fn method_name(&self) -> &'static str {
381        match &self.kind {
382            SecureCredentialKind::SqlServer { .. } => "SQL Server Authentication",
383            SecureCredentialKind::AzureAccessToken { .. } => "Azure AD Access Token",
384            #[cfg(feature = "azure-identity")]
385            SecureCredentialKind::AzureManagedIdentity { .. } => "Azure Managed Identity",
386            #[cfg(feature = "azure-identity")]
387            SecureCredentialKind::AzureServicePrincipal { .. } => "Azure Service Principal",
388            #[cfg(feature = "azure-identity")]
389            SecureCredentialKind::AzureDefault => "Azure Default Credential Chain",
390            #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
391            SecureCredentialKind::Integrated => "Integrated Authentication",
392            #[cfg(feature = "cert-auth")]
393            SecureCredentialKind::Certificate { .. } => "Certificate Authentication",
394        }
395    }
396
397    /// Get the username for SQL Server authentication.
398    ///
399    /// Returns `None` for non-SQL authentication methods.
400    #[must_use]
401    pub fn username(&self) -> Option<&str> {
402        match &self.kind {
403            SecureCredentialKind::SqlServer { username, .. } => Some(username),
404            _ => None,
405        }
406    }
407
408    /// Get the password for SQL Server authentication.
409    ///
410    /// Returns `None` for non-SQL authentication methods.
411    ///
412    /// # Security
413    ///
414    /// Be careful with the returned reference - avoid logging or
415    /// copying the value unnecessarily.
416    #[must_use]
417    pub fn password(&self) -> Option<&str> {
418        match &self.kind {
419            SecureCredentialKind::SqlServer { password, .. } => Some(password.expose_secret()),
420            _ => None,
421        }
422    }
423
424    /// Get the token for Azure AD authentication.
425    ///
426    /// Returns `None` for non-Azure AD authentication methods.
427    ///
428    /// # Security
429    ///
430    /// Be careful with the returned reference - avoid logging or
431    /// copying the value unnecessarily.
432    #[must_use]
433    pub fn token(&self) -> Option<&str> {
434        match &self.kind {
435            SecureCredentialKind::AzureAccessToken { token } => Some(token.expose_secret()),
436            _ => None,
437        }
438    }
439}
440
441#[cfg(feature = "zeroize")]
442impl std::fmt::Debug for SecureCredentials {
443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444        match &self.kind {
445            SecureCredentialKind::SqlServer { username, .. } => f
446                .debug_struct("SecureCredentials::SqlServer")
447                .field("username", username)
448                .field("password", &"[REDACTED]")
449                .finish(),
450            SecureCredentialKind::AzureAccessToken { .. } => f
451                .debug_struct("SecureCredentials::AzureAccessToken")
452                .field("token", &"[REDACTED]")
453                .finish(),
454            #[cfg(feature = "azure-identity")]
455            SecureCredentialKind::AzureManagedIdentity { client_id } => f
456                .debug_struct("SecureCredentials::AzureManagedIdentity")
457                .field("client_id", client_id)
458                .finish(),
459            #[cfg(feature = "azure-identity")]
460            SecureCredentialKind::AzureServicePrincipal {
461                tenant_id,
462                client_id,
463                ..
464            } => f
465                .debug_struct("SecureCredentials::AzureServicePrincipal")
466                .field("tenant_id", tenant_id)
467                .field("client_id", client_id)
468                .field("client_secret", &"[REDACTED]")
469                .finish(),
470            #[cfg(feature = "azure-identity")]
471            SecureCredentialKind::AzureDefault => {
472                f.debug_struct("SecureCredentials::AzureDefault").finish()
473            }
474            #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
475            SecureCredentialKind::Integrated => {
476                f.debug_struct("SecureCredentials::Integrated").finish()
477            }
478            #[cfg(feature = "cert-auth")]
479            SecureCredentialKind::Certificate {
480                tenant_id,
481                client_id,
482                cert_path,
483                ..
484            } => f
485                .debug_struct("SecureCredentials::Certificate")
486                .field("tenant_id", tenant_id)
487                .field("client_id", client_id)
488                .field("cert_path", cert_path)
489                .field("password", &"[REDACTED]")
490                .finish(),
491        }
492    }
493}
494
495/// Convert from non-secure credentials to secure credentials.
496#[cfg(feature = "zeroize")]
497impl From<Credentials> for SecureCredentials {
498    fn from(creds: Credentials) -> Self {
499        match creds {
500            Credentials::SqlServer { username, password } => {
501                SecureCredentials::sql_server(username.into_owned(), password.into_owned())
502            }
503            Credentials::AzureAccessToken { token } => {
504                SecureCredentials::azure_token(token.into_owned())
505            }
506            #[cfg(feature = "azure-identity")]
507            Credentials::AzureManagedIdentity { client_id } => SecureCredentials {
508                kind: SecureCredentialKind::AzureManagedIdentity {
509                    client_id: client_id.map(|c| c.into_owned()),
510                },
511            },
512            #[cfg(feature = "azure-identity")]
513            Credentials::AzureServicePrincipal {
514                tenant_id,
515                client_id,
516                client_secret,
517            } => SecureCredentials {
518                kind: SecureCredentialKind::AzureServicePrincipal {
519                    tenant_id: tenant_id.into_owned(),
520                    client_id: client_id.into_owned(),
521                    client_secret: SecretString::new(client_secret.into_owned()),
522                },
523            },
524            #[cfg(feature = "azure-identity")]
525            Credentials::AzureDefault => SecureCredentials {
526                kind: SecureCredentialKind::AzureDefault,
527            },
528            #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
529            Credentials::Integrated => SecureCredentials {
530                kind: SecureCredentialKind::Integrated,
531            },
532            #[cfg(feature = "cert-auth")]
533            Credentials::Certificate {
534                tenant_id,
535                client_id,
536                cert_path,
537                password,
538            } => SecureCredentials {
539                kind: SecureCredentialKind::Certificate {
540                    tenant_id: tenant_id.into_owned(),
541                    client_id: client_id.into_owned(),
542                    cert_path: cert_path.into_owned(),
543                    password: password.map(|p| SecretString::new(p.into_owned())),
544                },
545            },
546        }
547    }
548}
549
550#[cfg(test)]
551#[allow(clippy::panic)]
552mod tests {
553    use super::*;
554
555    #[test]
556    fn test_credentials_sql_server() {
557        let creds = Credentials::sql_server("user", "password");
558        assert!(creds.is_sql_auth());
559        assert!(!creds.is_azure_ad());
560        match creds {
561            Credentials::SqlServer { username, password } => {
562                assert_eq!(username.as_ref(), "user");
563                assert_eq!(password.as_ref(), "password");
564            }
565            _ => panic!("Expected SqlServer variant"),
566        }
567    }
568
569    #[test]
570    fn test_credentials_azure_token() {
571        let creds = Credentials::azure_token("my-token");
572        assert!(!creds.is_sql_auth());
573        assert!(creds.is_azure_ad());
574        match creds {
575            Credentials::AzureAccessToken { token } => {
576                assert_eq!(token.as_ref(), "my-token");
577            }
578            _ => panic!("Expected AzureAccessToken variant"),
579        }
580    }
581
582    #[test]
583    fn test_credentials_debug_redacts_password() {
584        let creds = Credentials::sql_server("user", "supersecret");
585        let debug = format!("{creds:?}");
586        assert!(debug.contains("user"));
587        assert!(!debug.contains("supersecret"));
588        assert!(debug.contains("REDACTED"));
589    }
590
591    #[test]
592    fn test_credentials_debug_redacts_token() {
593        let creds = Credentials::azure_token("supersecrettoken");
594        let debug = format!("{creds:?}");
595        assert!(!debug.contains("supersecrettoken"));
596        assert!(debug.contains("REDACTED"));
597    }
598
599    #[cfg(feature = "cert-auth")]
600    #[test]
601    fn test_credentials_certificate_constructor_and_debug() {
602        let creds =
603            Credentials::certificate("tenant-1", "client-1", "/path/app.pfx", Some("pw".into()));
604        assert!(!creds.is_sql_auth());
605        // Certificate auth is Entra-backed but `is_azure_ad()` reports the
606        // pre-acquired/MI/SP token variants only; cert is handled explicitly
607        // in the client's FEDAUTH validation.
608        assert!(!creds.is_azure_ad());
609        assert_eq!(creds.method_name(), "Certificate Authentication");
610        match &creds {
611            Credentials::Certificate {
612                tenant_id,
613                client_id,
614                cert_path,
615                ..
616            } => {
617                assert_eq!(tenant_id.as_ref(), "tenant-1");
618                assert_eq!(client_id.as_ref(), "client-1");
619                assert_eq!(cert_path.as_ref(), "/path/app.pfx");
620            }
621            _ => panic!("Expected Certificate variant"),
622        }
623        let debug = format!("{creds:?}");
624        assert!(debug.contains("tenant-1"));
625        assert!(debug.contains("/path/app.pfx"));
626        assert!(!debug.contains("pw"));
627        assert!(debug.contains("REDACTED"));
628    }
629
630    #[cfg(feature = "azure-identity")]
631    #[test]
632    fn test_credentials_azure_default() {
633        let creds = Credentials::azure_default();
634        assert!(creds.is_azure_ad());
635        assert!(!creds.is_sql_auth());
636        assert!(matches!(creds, Credentials::AzureDefault));
637        assert_eq!(creds.method_name(), "Azure Default Credential Chain");
638        assert_eq!(format!("{creds:?}"), "AzureDefault");
639    }
640
641    #[cfg(feature = "zeroize")]
642    mod zeroize_tests {
643        use super::*;
644
645        #[test]
646        fn test_secret_string_creation() {
647            let secret = SecretString::new("my-password");
648            assert_eq!(secret.expose_secret(), "my-password");
649        }
650
651        #[test]
652        fn test_secret_string_zeroize_clears_value() {
653            let mut secret = SecretString::new("super-secret");
654            secret.zeroize();
655            // `zeroize()` is what `ZeroizeOnDrop` runs on drop; it must scrub
656            // the secret. Reading the buffer after the value is dropped would
657            // be UB, so this asserts the scrubbing operation directly.
658            assert!(secret.expose_secret().is_empty());
659        }
660
661        #[test]
662        fn test_secret_string_is_zeroize_on_drop() {
663            // Compile-time guarantee that `SecretString` is scrubbed on drop.
664            fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
665            assert_zeroize_on_drop::<SecretString>();
666        }
667
668        #[test]
669        fn test_secret_string_from_string() {
670            let secret: SecretString = String::from("password").into();
671            assert_eq!(secret.expose_secret(), "password");
672        }
673
674        #[test]
675        fn test_secret_string_from_str() {
676            let secret: SecretString = "password".into();
677            assert_eq!(secret.expose_secret(), "password");
678        }
679
680        #[test]
681        fn test_secret_string_debug_redacted() {
682            let secret = SecretString::new("supersecret");
683            let debug = format!("{secret:?}");
684            assert!(!debug.contains("supersecret"));
685            assert!(debug.contains("REDACTED"));
686        }
687
688        #[test]
689        fn test_secret_string_clone() {
690            let secret = SecretString::new("password");
691            let cloned = secret.clone();
692            assert_eq!(cloned.expose_secret(), "password");
693        }
694
695        #[test]
696        fn test_secure_credentials_sql_server() {
697            let creds = SecureCredentials::sql_server("user", "password");
698            assert_eq!(creds.username(), Some("user"));
699            assert_eq!(creds.password(), Some("password"));
700            assert!(creds.token().is_none());
701        }
702
703        #[test]
704        fn test_secure_credentials_azure_token() {
705            let creds = SecureCredentials::azure_token("my-token");
706            assert!(creds.username().is_none());
707            assert!(creds.password().is_none());
708            assert_eq!(creds.token(), Some("my-token"));
709        }
710
711        #[test]
712        fn test_secure_credentials_debug_redacts_password() {
713            let creds = SecureCredentials::sql_server("user", "supersecret");
714            let debug = format!("{creds:?}");
715            assert!(debug.contains("user"));
716            assert!(!debug.contains("supersecret"));
717            assert!(debug.contains("REDACTED"));
718        }
719
720        #[test]
721        fn test_secure_credentials_debug_redacts_token() {
722            let creds = SecureCredentials::azure_token("supersecrettoken");
723            let debug = format!("{creds:?}");
724            assert!(!debug.contains("supersecrettoken"));
725            assert!(debug.contains("REDACTED"));
726        }
727
728        #[test]
729        fn test_secure_credentials_from_credentials() {
730            let creds = Credentials::sql_server("user", "password");
731            let secure: SecureCredentials = creds.into();
732            assert_eq!(secure.username(), Some("user"));
733            assert_eq!(secure.password(), Some("password"));
734        }
735
736        #[test]
737        fn test_secure_credentials_clone() {
738            let creds = SecureCredentials::sql_server("user", "password");
739            let cloned = creds.clone();
740            assert_eq!(cloned.username(), Some("user"));
741            assert_eq!(cloned.password(), Some("password"));
742        }
743    }
744}