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}