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}