Skip to main content

broadcast_auth/
authenticator.rs

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