Skip to main content

server_verify/
server_verify.rs

1//! Server-side challenge + verify (`broadcast_auth::Verifier`) — the origin
2//! half of the RFC 7235 handshake: issue a `WWW-Authenticate` challenge, then
3//! accept a correct `Authorization` response and reject a wrong one. Covers
4//! Digest in depth (the scheme with the most moving parts — nonce/`nc`/`HA1`/
5//! `HA2`), plus Basic, Bearer, and the reverse-proxy `Forwarded` scheme.
6//!
7//! Self-contained and non-blocking: no socket, no server actually run — just
8//! the `Verifier` API a real origin (e.g. multimux's shared output-auth
9//! middleware) would call.
10//!
11//! # Usage
12//!
13//! ```bash
14//! cargo run --example server_verify -p broadcast-auth
15//! ```
16
17use broadcast_auth::{AuthResult, Credentials, RequestContext, Verifier, respond};
18
19const REALM: &str = "cameras";
20
21fn main() {
22    digest();
23    basic();
24    bearer();
25    forwarded();
26}
27
28/// Digest (RFC 7616): the fullest round trip — challenge, correct response
29/// accepted, wrong password rejected.
30fn digest() {
31    let verifier = Verifier::new(
32        Credentials::Digest {
33            username: "admin".into(),
34            password: "hunter2".into(),
35        },
36        REALM,
37    );
38    let challenge = verifier.challenge();
39    println!("[digest] challenge: {challenge}");
40
41    let ctx = RequestContext::new("DESCRIBE", "rtsp://cam/live");
42
43    // Correct credential: respond() answers the challenge, verify() accepts.
44    let correct = respond(&challenge, &ctx, Credentials::new("admin", "hunter2"))
45        .expect("respond computes an Authorization value");
46    let outcome = verify(&verifier, &correct, &ctx);
47    println!("[digest] correct password  -> {outcome:?}");
48    assert_eq!(outcome, AuthResult::Ok);
49
50    // Wrong credential: same challenge, wrong password -> rejected.
51    let wrong = respond(&challenge, &ctx, Credentials::new("admin", "WRONG"))
52        .expect("respond computes an Authorization value even for a wrong password");
53    let outcome = verify(&verifier, &wrong, &ctx);
54    println!("[digest] wrong password    -> {outcome:?}");
55    assert_eq!(outcome, AuthResult::Unauthorized);
56}
57
58/// Basic (RFC 7617): same accept/reject shape, briefly.
59fn basic() {
60    let verifier = Verifier::new(
61        Credentials::Basic {
62            username: "admin".into(),
63            password: "hunter2".into(),
64        },
65        REALM,
66    );
67    let challenge = verifier.challenge();
68    println!("[basic] challenge: {challenge}");
69
70    let ctx = RequestContext::new("GET", "/stream/media.m3u8");
71    let correct =
72        respond(&challenge, &ctx, Credentials::new("admin", "hunter2")).expect("responds");
73    assert_eq!(verify(&verifier, &correct, &ctx), AuthResult::Ok);
74    println!("[basic] correct password   -> Ok");
75
76    let wrong = respond(&challenge, &ctx, Credentials::new("admin", "WRONG")).expect("responds");
77    assert_eq!(verify(&verifier, &wrong, &ctx), AuthResult::Unauthorized);
78    println!("[basic] wrong password     -> Unauthorized");
79}
80
81/// Bearer (RFC 6750): no challenge round-trip needed, but still an
82/// accept/reject pair — a wrong token must not verify.
83fn bearer() {
84    let verifier = Verifier::new(Credentials::bearer("right-token"), REALM);
85    let challenge = verifier.challenge();
86    println!("[bearer] challenge: {challenge}");
87
88    let ctx = RequestContext::new("GET", "/stream/media.m3u8");
89    let correct = respond(&challenge, &ctx, Credentials::bearer("right-token")).expect("responds");
90    assert_eq!(verify(&verifier, &correct, &ctx), AuthResult::Ok);
91    println!("[bearer] correct token     -> Ok");
92
93    let wrong = respond(&challenge, &ctx, Credentials::bearer("wrong-token")).expect("responds");
94    assert_eq!(verify(&verifier, &wrong, &ctx), AuthResult::Unauthorized);
95    println!("[bearer] wrong token       -> Unauthorized");
96}
97
98/// Reverse-proxy forwarded-auth (`Verifier::forwarded`): no credential at
99/// all — authenticated iff the proxy-set user header is present and
100/// non-empty. See `Verifier::forwarded`'s doc for the trust assumption this
101/// scheme relies on (only safe behind a proxy that strips client-supplied
102/// copies of the header).
103fn forwarded() {
104    let verifier = Verifier::forwarded("X-Forwarded-User", Some("X-Forwarded-For".to_string()));
105    println!("[forwarded] challenge: {}", verifier.challenge());
106
107    let headers: &[(&str, &str)] = &[("X-Forwarded-User", "alice")];
108    let ctx = RequestContext::new("GET", "/stream/media.m3u8").with_headers(headers);
109    assert_eq!(verifier.verify(&ctx), AuthResult::Ok);
110    println!("[forwarded] header present -> Ok");
111
112    let ctx_no_header = RequestContext::new("GET", "/stream/media.m3u8");
113    assert_eq!(verifier.verify(&ctx_no_header), AuthResult::Unauthorized);
114    println!("[forwarded] header absent  -> Unauthorized");
115}
116
117/// Builds a request context carrying `authorization` as the `Authorization`
118/// header, then verifies it against `verifier`.
119fn verify(verifier: &Verifier, authorization: &str, ctx: &RequestContext<'_>) -> AuthResult {
120    let headers: &[(&str, &str)] = &[("authorization", authorization)];
121    let ctx_with_auth = RequestContext::new(ctx.method, ctx.uri).with_headers(headers);
122    verifier.verify(&ctx_with_auth)
123}