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}