Skip to main content

broadcast_auth/
credentials.rs

1//! The scheme-agnostic credential model shared by every auth-facing client.
2
3/// Credentials for one of the supported auth schemes.
4///
5/// `Basic` and `Digest` both carry a username/password and are treated
6/// identically by [`crate::respond`]/[`crate::Authenticator`]: which wire
7/// scheme is actually used is decided by the server's challenge, not by which
8/// variant was constructed (RFC 7235 content negotiation — a server may offer
9/// either, or both, in `WWW-Authenticate`). Use [`Credentials::new`] for the
10/// common password case; construct `Basic`/`Digest` directly only when the
11/// caller must pin one scheme.
12#[non_exhaustive]
13#[derive(Clone, PartialEq, Eq)]
14pub enum Credentials {
15    /// Username/password for HTTP Basic auth (RFC 7617) — or RTSP's reuse of
16    /// it (RFC 2326 §14/§16).
17    Basic {
18        /// Account username.
19        username: String,
20        /// Account password.
21        password: String,
22    },
23    /// Username/password for HTTP Digest auth (RFC 7616) — or RTSP's reuse of
24    /// it (RFC 2326 §14).
25    Digest {
26        /// Account username.
27        username: String,
28        /// Account password.
29        password: String,
30    },
31    /// A bearer token (RFC 6750) — sent verbatim as `Authorization: Bearer
32    /// <token>` with no challenge round-trip required.
33    Bearer {
34        /// The opaque bearer token.
35        token: String,
36    },
37}
38
39impl Credentials {
40    /// Convenience constructor for the common password-based case.
41    ///
42    /// Does not commit to Basic or Digest: [`crate::respond`]/
43    /// [`crate::Authenticator::from_challenge`] answer whichever scheme the
44    /// server's `WWW-Authenticate` challenge advertises. Internally this
45    /// builds a `Digest` value (the superset — `http-auth`'s challenge
46    /// parser inspects the challenge itself, not this variant, to pick the
47    /// wire scheme), so `new("u", "p")` behaves identically to a
48    /// hand-constructed `Basic`/`Digest` with the same username/password.
49    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
50        Credentials::Digest {
51            username: username.into(),
52            password: password.into(),
53        }
54    }
55
56    /// Constructs a bearer-token credential (RFC 6750).
57    pub fn bearer(token: impl Into<String>) -> Self {
58        Credentials::Bearer {
59            token: token.into(),
60        }
61    }
62
63    /// Returns the `(username, password)` pair for a password-based scheme,
64    /// or `None` for `Bearer`.
65    pub(crate) fn username_password(&self) -> Option<(&str, &str)> {
66        match self {
67            Credentials::Basic { username, password }
68            | Credentials::Digest { username, password } => Some((username, password)),
69            Credentials::Bearer { .. } => None,
70        }
71    }
72}
73
74/// Manual `Debug` (rather than `#[derive(Debug)]`): every scheme carries a
75/// secret (`password`/`token`) that must never render verbatim. Usernames are
76/// not secret and are shown as-is to keep the output useful for diagnostics.
77impl core::fmt::Debug for Credentials {
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        match self {
80            Credentials::Basic { username, .. } => f
81                .debug_struct("Credentials::Basic")
82                .field("username", username)
83                .field("password", &"***")
84                .finish(),
85            Credentials::Digest { username, .. } => f
86                .debug_struct("Credentials::Digest")
87                .field("username", username)
88                .field("password", &"***")
89                .finish(),
90            Credentials::Bearer { .. } => f
91                .debug_struct("Credentials::Bearer")
92                .field("token", &"***")
93                .finish(),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    // Security-blocker regression (pre-release audit): `Credentials` must
103    // never render a secret via `{:?}`. Must FAIL if `Debug` reverts to a
104    // plain `#[derive(Debug)]`.
105    #[test]
106    fn basic_debug_redacts_password_but_keeps_username() {
107        let creds = Credentials::Basic {
108            username: "admin".to_string(),
109            password: "s3cr3t-password".to_string(),
110        };
111        let debug = format!("{creds:?}");
112        assert!(!debug.contains("s3cr3t-password"), "leaked: {debug}");
113        assert!(
114            debug.contains("admin"),
115            "username should be visible: {debug}"
116        );
117        assert!(debug.contains("***"), "expected redaction marker: {debug}");
118    }
119
120    #[test]
121    fn digest_debug_redacts_password_but_keeps_username() {
122        let creds = Credentials::Digest {
123            username: "camera-user".to_string(),
124            password: "hunter2-super-secret".to_string(),
125        };
126        let debug = format!("{creds:?}");
127        assert!(!debug.contains("hunter2-super-secret"), "leaked: {debug}");
128        assert!(
129            debug.contains("camera-user"),
130            "username should be visible: {debug}"
131        );
132        assert!(debug.contains("***"), "expected redaction marker: {debug}");
133    }
134
135    #[test]
136    fn bearer_debug_redacts_token() {
137        let creds = Credentials::bearer("super-secret-bearer-token-xyz");
138        let debug = format!("{creds:?}");
139        assert!(
140            !debug.contains("super-secret-bearer-token-xyz"),
141            "leaked: {debug}"
142        );
143        assert!(debug.contains("***"), "expected redaction marker: {debug}");
144    }
145}