Skip to main content

broadcast_auth/
authenticator.rs

1//! Stateful challenge->response computation across a session.
2
3use http_auth::{PasswordClient, PasswordParams};
4
5use crate::credentials::Credentials;
6use crate::error::{Error, Result};
7use crate::request::RequestContext;
8
9/// Per-scheme state an [`Authenticator`] carries between calls.
10///
11/// Basic/Digest need the negotiated [`PasswordClient`] kept alive so Digest's
12/// `nc` (nonce count) advances correctly across successive requests in the
13/// same session (RFC 7616 §3.3); Bearer is stateless.
14enum SchemeState {
15    Password(PasswordClient),
16    Bearer,
17}
18
19/// Negotiates a challenge once, then answers every subsequent request in the
20/// session (RFC 7235 `WWW-Authenticate`/`Authorization`; RFC 2326 §14 for
21/// RTSP; RFC 6750 for Bearer).
22///
23/// Construct with [`Authenticator::from_challenge`] on the first `401`/`407`,
24/// then call [`Authenticator::authorization`] for every outgoing request —
25/// including the immediate retry. Call `from_challenge` again if the server
26/// re-challenges with a fresh nonce (`stale=true`) to pick it up.
27pub struct Authenticator {
28    credentials: Credentials,
29    state: SchemeState,
30}
31
32impl Authenticator {
33    /// Builds an authenticator from a `WWW-Authenticate` challenge value and
34    /// the caller's credentials.
35    ///
36    /// For `Credentials::Basic`/`Digest`, the challenge is parsed by
37    /// [`http_auth`], which itself decides the wire scheme (Basic or Digest)
38    /// from the challenge content — not from which `Credentials` variant was
39    /// passed in. For `Credentials::Bearer`, the challenge value is not
40    /// inspected: RFC 6750 needs no challenge round-trip, so the token is
41    /// used as-is.
42    pub fn from_challenge(www_authenticate: &str, credentials: Credentials) -> Result<Self> {
43        let state = match &credentials {
44            Credentials::Bearer { .. } => SchemeState::Bearer,
45            Credentials::Basic { .. } | Credentials::Digest { .. } => {
46                let client = PasswordClient::try_from(www_authenticate)
47                    .map_err(|e| Error::ChallengeParse(e.to_string()))?;
48                SchemeState::Password(client)
49            }
50        };
51        Ok(Authenticator { credentials, state })
52    }
53
54    /// Computes the `Authorization` header value for `ctx`.
55    ///
56    /// For Basic/Digest this advances the Digest nonce count on every call
57    /// (RFC 7616 §3.3); for Bearer it always returns `Bearer <token>` (RFC
58    /// 6750).
59    pub fn authorization(&mut self, ctx: &RequestContext<'_>) -> Result<String> {
60        match &mut self.state {
61            SchemeState::Bearer => {
62                let Credentials::Bearer { token } = &self.credentials else {
63                    unreachable!("SchemeState::Bearer only paired with Credentials::Bearer")
64                };
65                Ok(format!("Bearer {token}"))
66            }
67            SchemeState::Password(client) => {
68                let (username, password) = self
69                    .credentials
70                    .username_password()
71                    .expect("SchemeState::Password only paired with Basic/Digest credentials");
72                client
73                    .respond(&PasswordParams {
74                        username,
75                        password,
76                        uri: ctx.uri,
77                        method: ctx.method,
78                        body: ctx.body,
79                    })
80                    .map_err(|e| Error::ResponseCompute(e.to_string()))
81            }
82        }
83    }
84}
85
86impl core::fmt::Debug for Authenticator {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        // PasswordClient is not Debug; avoid leaking the password/token too.
89        let scheme = match &self.credentials {
90            Credentials::Basic { .. } => "Basic",
91            Credentials::Digest { .. } => "Digest",
92            Credentials::Bearer { .. } => "Bearer",
93        };
94        f.debug_struct("Authenticator")
95            .field("scheme", &scheme)
96            .finish_non_exhaustive()
97    }
98}
99
100/// One-shot challenge->response: computes the `Authorization` value for a
101/// single request without keeping an [`Authenticator`] around.
102///
103/// Prefer [`Authenticator`] when the same credentials answer multiple
104/// requests in one session (Digest's `nc` must advance) — this is a thin
105/// convenience over `Authenticator::from_challenge(..)?.authorization(..)`.
106pub fn respond(
107    www_authenticate: &str,
108    ctx: &RequestContext<'_>,
109    credentials: Credentials,
110) -> Result<String> {
111    Authenticator::from_challenge(www_authenticate, credentials)?.authorization(ctx)
112}