Skip to main content

churust_auth/
lib.rs

1//! Authentication plugins for the [Churust](churust_core) web framework.
2//!
3//! This crate provides ready-made authentication schemes — [`Bearer`] tokens,
4//! HTTP [`Basic`] credentials, and [`Jwt`] bearer tokens — plus the
5//! [`Principal<P>`] extractor used to require and read the authenticated user
6//! inside a handler.
7//!
8//! # Authentication vs. authorization
9//!
10//! An auth plugin only *authenticates*: it inspects the incoming `Authorization`
11//! header, verifies the credentials, and — on success — inserts a *principal*
12//! value (your own user/claims type) into the call's extensions. It never
13//! rejects a request on its own. If the credentials are missing or invalid, the
14//! plugin simply leaves no principal behind.
15//!
16//! *Authorization* — actually requiring a logged-in user — is type-driven and
17//! explicit: a handler asks for a [`Principal<P>`] argument. When a principal of
18//! type `P` is present the handler runs; when it is absent the extractor returns
19//! `401 Unauthorized` with a `WWW-Authenticate: Bearer` challenge before the
20//! handler body is ever entered. Routes that do not ask for a `Principal` stay
21//! public.
22//!
23//! All schemes are constructed through the [`Auth`] namespace and installed with
24//! [`AppBuilder::install`](churust_core::AppBuilder::install). The principal type
25//! `P` you choose is what later handlers extract; only one principal type can be
26//! resolved per call (the most recently inserted value of a given type wins).
27//!
28//! # Example
29//!
30//! Protect a route with bearer-token auth. The `/me` handler only runs when a
31//! valid token resolved to a `User` principal; otherwise the [`Principal`]
32//! extractor short-circuits with `401`.
33//!
34//! ```
35//! use churust_core::{Churust, TestClient};
36//! use churust_auth::{Auth, Principal};
37//!
38//! #[derive(Clone)]
39//! struct User {
40//!     name: String,
41//! }
42//!
43//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
44//! let app = Churust::server()
45//!     .install(Auth::bearer(|token: String| async move {
46//!         (token == "s3cret").then(|| User { name: "ana".into() })
47//!     }))
48//!     .routing(|r| {
49//!         r.get("/me", |Principal(u): Principal<User>| async move {
50//!             format!("hello {}", u.name)
51//!         });
52//!     })
53//!     .build();
54//!
55//! // A valid token reaches the handler.
56//! let ok = TestClient::new(app.clone())
57//!     .get("/me")
58//!     .header("authorization", "Bearer s3cret")
59//!     .send()
60//!     .await;
61//! assert_eq!(ok.status().as_u16(), 200);
62//! assert_eq!(ok.text(), "hello ana");
63//!
64//! // No credentials => 401 with a challenge, handler never runs.
65//! let denied = TestClient::new(app).get("/me").send().await;
66//! assert_eq!(denied.status().as_u16(), 401);
67//! assert_eq!(denied.header("www-authenticate"), Some("Bearer"));
68//! # });
69//! ```
70#![deny(missing_docs)]
71
72use async_trait::async_trait;
73use churust_core::{
74    AppBuilder, Call, Error, FromCallParts, Middleware, Next, Phase, Plugin, Response, Result,
75};
76use http::header::WWW_AUTHENTICATE;
77use http::{HeaderValue, StatusCode};
78use std::future::Future;
79use std::marker::PhantomData;
80use std::pin::Pin;
81use std::sync::Arc;
82
83type BoxFut<T> = Pin<Box<dyn Future<Output = T> + Send>>;
84
85/// Handler argument that requires and yields the authenticated principal of
86/// type `P`.
87///
88/// `Principal<P>` is the bridge between *authenticating* (a scheme plugin
89/// inserting a principal into the call) and *authorizing* (a handler demanding
90/// one). Add it as a handler parameter to make that route require auth: the
91/// extractor looks up a value of type `P` previously stored on the call by an
92/// auth plugin such as [`Auth::bearer`], [`Auth::basic`], or [`Auth::jwt_hs256`].
93///
94/// The inner value is exposed as the public tuple field, so you typically
95/// destructure it directly in the parameter list, e.g.
96/// `|Principal(user): Principal<User>|`.
97///
98/// # Type parameter
99///
100/// * `P` — the principal type you chose when installing the auth scheme. It must
101///   be `Clone + Send + Sync + 'static`. This is the same type the plugin's
102///   `verify` closure returns (or, for [`Jwt`], the decoded claims type).
103///
104/// # Errors
105///
106/// If no principal of type `P` was inserted for this call (no/invalid/expired
107/// credentials, or a mismatched principal type) the extractor returns
108/// `401 Unauthorized` with a `WWW-Authenticate: Bearer` response header, and the
109/// handler body never runs.
110///
111/// # Examples
112///
113/// ```
114/// use churust_core::{Churust, TestClient};
115/// use churust_auth::{Auth, Principal};
116///
117/// #[derive(Clone)]
118/// struct User {
119///     id: u64,
120/// }
121///
122/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
123/// let app = Churust::server()
124///     .install(Auth::bearer(|tok: String| async move {
125///         (tok == "ok").then(|| User { id: 7 })
126///     }))
127///     .routing(|r| {
128///         r.get("/id", |Principal(u): Principal<User>| async move {
129///             u.id.to_string()
130///         });
131///     })
132///     .build();
133///
134/// let res = TestClient::new(app)
135///     .get("/id")
136///     .header("authorization", "Bearer ok")
137///     .send()
138///     .await;
139/// assert_eq!(res.status().as_u16(), 200);
140/// assert_eq!(res.text(), "7");
141/// # });
142/// ```
143#[derive(Debug, Clone)]
144pub struct Principal<P>(
145    /// The authenticated principal value inserted by the auth scheme.
146    pub P,
147);
148
149#[async_trait]
150impl<P> FromCallParts for Principal<P>
151where
152    P: Clone + Send + Sync + 'static,
153{
154    async fn from_call_parts(call: &mut Call) -> Result<Self> {
155        match call.get::<P>() {
156            Some(p) => Ok(Principal(p)),
157            None => Err(
158                Error::new(StatusCode::UNAUTHORIZED, "authentication required")
159                    .with_response_header(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer")),
160            ),
161        }
162    }
163}
164
165// ---------- Bearer ----------
166
167/// Bearer-token authentication plugin produced by [`Auth::bearer`].
168///
169/// On every request this plugin reads the `Authorization: Bearer <token>`
170/// header (the scheme word is matched case-insensitively), trims the token, and
171/// calls your `verify` closure. If the closure returns `Some(principal)`, that
172/// principal is inserted into the call so a later [`Principal<P>`] extractor can
173/// pick it up. A missing header, a non-bearer scheme, or a `None` result simply
174/// leaves the call unauthenticated — it is never rejected here; rejection is the
175/// job of [`Principal<P>`].
176///
177/// You rarely name this type directly; construct it with [`Auth::bearer`] and
178/// hand it to [`AppBuilder::install`](churust_core::AppBuilder::install).
179///
180/// # Type parameters
181///
182/// * `P` — the principal type the closure resolves to.
183/// * `F` — the verifier closure type (inferred from the argument).
184///
185/// # Examples
186///
187/// ```
188/// use churust_core::{Churust, TestClient};
189/// use churust_auth::{Auth, Principal};
190///
191/// #[derive(Clone)]
192/// struct User;
193///
194/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
195/// let app = Churust::server()
196///     .install(Auth::bearer(|t: String| async move { (t == "ok").then_some(User) }))
197///     .routing(|r| {
198///         r.get("/", |_p: Principal<User>| async { "in" });
199///     })
200///     .build();
201///
202/// let res = TestClient::new(app)
203///     .get("/")
204///     .header("authorization", "Bearer ok")
205///     .send()
206///     .await;
207/// assert_eq!(res.status().as_u16(), 200);
208/// # });
209/// ```
210pub struct Bearer<P, F> {
211    verify: Arc<F>,
212    _p: PhantomData<fn() -> P>,
213}
214
215/// Constructor namespace for the authentication scheme plugins.
216///
217/// `Auth` is a zero-sized type that groups the factory functions for every
218/// scheme this crate offers. You never instantiate it; call its associated
219/// functions and pass the result to
220/// [`AppBuilder::install`](churust_core::AppBuilder::install):
221///
222/// * [`Auth::bearer`] — opaque `Authorization: Bearer` tokens verified by a closure.
223/// * [`Auth::basic`] — HTTP Basic `username:password` credentials verified by a closure.
224/// * [`Auth::jwt_hs256`] — HS256-signed JWTs decoded into a claims principal.
225///
226/// # Examples
227///
228/// ```
229/// use churust_core::Churust;
230/// use churust_auth::Auth;
231///
232/// #[derive(Clone)]
233/// struct User;
234///
235/// // Each factory yields a plugin ready to `.install(..)`.
236/// let _app = Churust::server()
237///     .install(Auth::bearer(|_t: String| async { Some(User) }))
238///     .build();
239/// ```
240pub struct Auth;
241
242impl Auth {
243    /// Builds a [`Bearer`] plugin that authenticates `Authorization: Bearer`
244    /// tokens.
245    ///
246    /// `verify` is invoked with the raw token string (without the `Bearer `
247    /// prefix, trimmed) and returns a future resolving to `Some(principal)` when
248    /// the token is valid or `None` when it is not. The returned principal `P`
249    /// is what handlers later read via [`Principal<P>`].
250    ///
251    /// # Parameters
252    ///
253    /// * `verify` — async closure `Fn(String) -> impl Future<Output = Option<P>>`,
254    ///   run once per request that carries a bearer token.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use churust_core::{Churust, TestClient};
260    /// use churust_auth::{Auth, Principal};
261    ///
262    /// #[derive(Clone)]
263    /// struct User { name: String }
264    ///
265    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
266    /// let app = Churust::server()
267    ///     .install(Auth::bearer(|token: String| async move {
268    ///         (token == "letmein").then(|| User { name: "ada".into() })
269    ///     }))
270    ///     .routing(|r| {
271    ///         r.get("/me", |Principal(u): Principal<User>| async move { u.name });
272    ///     })
273    ///     .build();
274    ///
275    /// let res = TestClient::new(app)
276    ///     .get("/me")
277    ///     .header("authorization", "Bearer letmein")
278    ///     .send()
279    ///     .await;
280    /// assert_eq!(res.text(), "ada");
281    /// # });
282    /// ```
283    pub fn bearer<P, F, Fut>(verify: F) -> Bearer<P, F>
284    where
285        F: Fn(String) -> Fut + Send + Sync + 'static,
286        Fut: Future<Output = Option<P>> + Send + 'static,
287        P: Clone + Send + Sync + 'static,
288    {
289        Bearer {
290            verify: Arc::new(verify),
291            _p: PhantomData,
292        }
293    }
294
295    /// Builds a [`Basic`] plugin that authenticates HTTP Basic credentials.
296    ///
297    /// On each request the plugin decodes the `Authorization: Basic <base64>`
298    /// header into a `username` and `password` and calls `verify`. A return of
299    /// `Some(principal)` authenticates the call; `None`, a malformed header, or a
300    /// missing header leaves it unauthenticated. The decoded principal `P` is
301    /// read by handlers via [`Principal<P>`].
302    ///
303    /// # Parameters
304    ///
305    /// * `verify` — async closure `Fn(String, String) -> impl Future<Output = Option<P>>`
306    ///   receiving `(username, password)`.
307    ///
308    /// # Gotcha
309    ///
310    /// HTTP Basic transmits the password in reversibly-encoded (base64) form, so
311    /// only use it over TLS.
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use churust_core::{Churust, TestClient};
317    /// use churust_auth::{Auth, Principal};
318    ///
319    /// #[derive(Clone)]
320    /// struct User { name: String }
321    ///
322    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
323    /// let app = Churust::server()
324    ///     .install(Auth::basic(|u: String, p: String| async move {
325    ///         (u == "admin" && p == "pw").then(|| User { name: u })
326    ///     }))
327    ///     .routing(|r| {
328    ///         r.get("/me", |Principal(u): Principal<User>| async move { u.name });
329    ///     })
330    ///     .build();
331    ///
332    /// // base64("admin:pw") == "YWRtaW46cHc="
333    /// let res = TestClient::new(app)
334    ///     .get("/me")
335    ///     .header("authorization", "Basic YWRtaW46cHc=")
336    ///     .send()
337    ///     .await;
338    /// assert_eq!(res.text(), "admin");
339    /// # });
340    /// ```
341    pub fn basic<P, F, Fut>(verify: F) -> Basic<P, F>
342    where
343        F: Fn(String, String) -> Fut + Send + Sync + 'static,
344        Fut: Future<Output = Option<P>> + Send + 'static,
345        P: Clone + Send + Sync + 'static,
346    {
347        Basic {
348            verify: Arc::new(verify),
349            _p: PhantomData,
350        }
351    }
352}
353
354impl<P, F, Fut> Plugin for Bearer<P, F>
355where
356    F: Fn(String) -> Fut + Send + Sync + 'static,
357    Fut: Future<Output = Option<P>> + Send + 'static,
358    P: Clone + Send + Sync + 'static,
359{
360    fn install(self: Box<Self>, app: &mut AppBuilder) {
361        let verify = self.verify.clone();
362        app.add_middleware_in(
363            Phase::Plugins,
364            Arc::new(BearerMiddleware::<P> {
365                verify: Arc::new(move |token: String| {
366                    let verify = verify.clone();
367                    Box::pin(async move { verify(token).await }) as BoxFut<Option<P>>
368                }),
369            }),
370        );
371    }
372}
373
374struct BearerMiddleware<P> {
375    #[allow(clippy::type_complexity)]
376    verify: Arc<dyn Fn(String) -> BoxFut<Option<P>> + Send + Sync>,
377}
378
379#[async_trait]
380impl<P> Middleware for BearerMiddleware<P>
381where
382    P: Clone + Send + Sync + 'static,
383{
384    async fn handle(&self, mut call: Call, next: Next) -> Response {
385        if let Some(raw) = call.header("authorization") {
386            if let Some(token) = raw
387                .strip_prefix("Bearer ")
388                .or_else(|| raw.strip_prefix("bearer "))
389            {
390                if let Some(principal) = (self.verify)(token.trim().to_string()).await {
391                    call.insert(principal);
392                }
393            }
394        }
395        next.run(call).await
396    }
397}
398
399// ---------- Basic ----------
400
401/// HTTP Basic authentication plugin produced by [`Auth::basic`].
402///
403/// On every request this plugin reads `Authorization: Basic <base64>` (scheme
404/// matched case-insensitively), base64-decodes it, splits the `username:password`
405/// pair on the first `:`, and calls your `verify` closure. `Some(principal)`
406/// authenticates the call by inserting the principal for a later
407/// [`Principal<P>`] extractor; `None`, a header that is not valid base64, or one
408/// without a `:` leaves the call unauthenticated. As with all schemes in this
409/// crate it never rejects the request itself.
410///
411/// Construct it with [`Auth::basic`] rather than naming this type directly. Note
412/// that Basic credentials are only base64-encoded, not encrypted, so serve such
413/// routes over TLS.
414///
415/// # Type parameters
416///
417/// * `P` — the principal type the closure resolves to.
418/// * `F` — the verifier closure type (inferred from the argument).
419///
420/// # Examples
421///
422/// ```
423/// use churust_core::{Churust, TestClient};
424/// use churust_auth::{Auth, Principal};
425///
426/// #[derive(Clone)]
427/// struct User { name: String }
428///
429/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
430/// let app = Churust::server()
431///     .install(Auth::basic(|u: String, p: String| async move {
432///         (p == "hunter2").then(|| User { name: u })
433///     }))
434///     .routing(|r| {
435///         r.get("/me", |Principal(u): Principal<User>| async move { u.name });
436///     })
437///     .build();
438///
439/// // base64("ada:hunter2") == "YWRhOmh1bnRlcjI="
440/// let res = TestClient::new(app)
441///     .get("/me")
442///     .header("authorization", "Basic YWRhOmh1bnRlcjI=")
443///     .send()
444///     .await;
445/// assert_eq!(res.text(), "ada");
446/// # });
447/// ```
448pub struct Basic<P, F> {
449    verify: Arc<F>,
450    _p: PhantomData<fn() -> P>,
451}
452
453impl<P, F, Fut> Plugin for Basic<P, F>
454where
455    F: Fn(String, String) -> Fut + Send + Sync + 'static,
456    Fut: Future<Output = Option<P>> + Send + 'static,
457    P: Clone + Send + Sync + 'static,
458{
459    fn install(self: Box<Self>, app: &mut AppBuilder) {
460        let verify = self.verify.clone();
461        app.add_middleware_in(
462            Phase::Plugins,
463            Arc::new(BasicMiddleware::<P> {
464                verify: Arc::new(move |u: String, p: String| {
465                    let verify = verify.clone();
466                    Box::pin(async move { verify(u, p).await }) as BoxFut<Option<P>>
467                }),
468            }),
469        );
470    }
471}
472
473struct BasicMiddleware<P> {
474    #[allow(clippy::type_complexity)]
475    verify: Arc<dyn Fn(String, String) -> BoxFut<Option<P>> + Send + Sync>,
476}
477
478#[async_trait]
479impl<P> Middleware for BasicMiddleware<P>
480where
481    P: Clone + Send + Sync + 'static,
482{
483    async fn handle(&self, mut call: Call, next: Next) -> Response {
484        if let Some((user, pass)) = call.header("authorization").and_then(decode_basic) {
485            if let Some(principal) = (self.verify)(user, pass).await {
486                call.insert(principal);
487            }
488        }
489        next.run(call).await
490    }
491}
492
493fn decode_basic(header: &str) -> Option<(String, String)> {
494    use base64::Engine;
495    let b64 = header
496        .strip_prefix("Basic ")
497        .or_else(|| header.strip_prefix("basic "))?;
498    let decoded = base64::engine::general_purpose::STANDARD
499        .decode(b64.trim())
500        .ok()?;
501    let text = String::from_utf8(decoded).ok()?;
502    let (user, pass) = text.split_once(':')?;
503    Some((user.to_string(), pass.to_string()))
504}
505
506// ---------- JWT ----------
507
508/// JWT bearer-token authentication plugin produced by [`Auth::jwt_hs256`].
509///
510/// This plugin reads `Authorization: Bearer <jwt>` (scheme matched
511/// case-insensitively), verifies the token's signature and standard claims
512/// against the configured key and validation, and on success inserts the decoded
513/// claims of type `C` as the principal. Handlers then read those claims via
514/// [`Principal<C>`]. A missing token, a bad signature, or a failed validation
515/// (e.g. an expired `exp`) leaves the call unauthenticated rather than erroring.
516///
517/// Unlike [`Bearer`] there is no `verify` closure: trust comes from the
518/// cryptographic signature, and the claims are deserialized directly into `C`.
519/// Construct it with [`Auth::jwt_hs256`].
520///
521/// # Type parameter
522///
523/// * `C` — the claims type, which must be
524///   `serde::de::DeserializeOwned + Clone + Send + Sync + 'static`. By default
525///   `jsonwebtoken` validates `exp`, so include an `exp` field in `C`.
526///
527/// # Examples
528///
529/// ```
530/// use churust_core::{Churust, TestClient};
531/// use churust_auth::{Auth, Principal};
532/// use jsonwebtoken::{encode, EncodingKey, Header};
533/// use serde::{Deserialize, Serialize};
534///
535/// #[derive(Serialize, Deserialize, Clone)]
536/// struct Claims { sub: String, exp: usize }
537///
538/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
539/// let secret = b"shhh";
540/// let token = encode(
541///     &Header::default(),
542///     &Claims { sub: "u1".into(), exp: 9_999_999_999 },
543///     &EncodingKey::from_secret(secret),
544/// ).unwrap();
545///
546/// let app = Churust::server()
547///     .install(Auth::jwt_hs256::<Claims>(secret))
548///     .routing(|r| {
549///         r.get("/who", |Principal(c): Principal<Claims>| async move { c.sub });
550///     })
551///     .build();
552///
553/// let res = TestClient::new(app)
554///     .get("/who")
555///     .header("authorization", &format!("Bearer {token}"))
556///     .send()
557///     .await;
558/// assert_eq!(res.text(), "u1");
559/// # });
560/// ```
561pub struct Jwt<C> {
562    key: jsonwebtoken::DecodingKey,
563    validation: jsonwebtoken::Validation,
564    _c: PhantomData<fn() -> C>,
565}
566
567impl Auth {
568    /// Builds a [`Jwt`] plugin that verifies HS256-signed JWT bearer tokens with
569    /// an HMAC secret.
570    ///
571    /// Uses the default `jsonwebtoken` validation for the `HS256` algorithm
572    /// (which, among other things, requires and checks an `exp` claim). Valid
573    /// tokens are decoded into the claims type `C` and inserted as the principal
574    /// for handlers to read with [`Principal<C>`].
575    ///
576    /// # Parameters
577    ///
578    /// * `secret` — the shared HMAC secret bytes used to sign and verify tokens.
579    ///   The same bytes must be used by whatever issues the tokens.
580    ///
581    /// # Gotcha
582    ///
583    /// Because default validation enforces `exp`, a claims type without an `exp`
584    /// field (or with one in the past) will fail to validate and leave the call
585    /// unauthenticated.
586    ///
587    /// # Examples
588    ///
589    /// ```
590    /// use churust_core::Churust;
591    /// use churust_auth::Auth;
592    /// use serde::{Deserialize, Serialize};
593    ///
594    /// #[derive(Serialize, Deserialize, Clone)]
595    /// struct Claims { sub: String, exp: usize }
596    ///
597    /// let _app = Churust::server()
598    ///     .install(Auth::jwt_hs256::<Claims>(b"shared-secret"))
599    ///     .build();
600    /// ```
601    pub fn jwt_hs256<C>(secret: &[u8]) -> Jwt<C>
602    where
603        C: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
604    {
605        Jwt {
606            key: jsonwebtoken::DecodingKey::from_secret(secret),
607            validation: jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
608            _c: PhantomData,
609        }
610    }
611}
612
613impl<C> Plugin for Jwt<C>
614where
615    C: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
616{
617    fn install(self: Box<Self>, app: &mut AppBuilder) {
618        app.add_middleware_in(
619            Phase::Plugins,
620            Arc::new(JwtMiddleware::<C> {
621                key: Arc::new(self.key),
622                validation: Arc::new(self.validation),
623                _c: PhantomData,
624            }),
625        );
626    }
627}
628
629struct JwtMiddleware<C> {
630    key: Arc<jsonwebtoken::DecodingKey>,
631    validation: Arc<jsonwebtoken::Validation>,
632    _c: PhantomData<fn() -> C>,
633}
634
635#[async_trait]
636impl<C> Middleware for JwtMiddleware<C>
637where
638    C: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
639{
640    async fn handle(&self, mut call: Call, next: Next) -> Response {
641        if let Some(raw) = call.header("authorization") {
642            if let Some(token) = raw
643                .strip_prefix("Bearer ")
644                .or_else(|| raw.strip_prefix("bearer "))
645            {
646                if let Ok(data) =
647                    jsonwebtoken::decode::<C>(token.trim(), &self.key, &self.validation)
648                {
649                    call.insert(data.claims);
650                }
651            }
652        }
653        next.run(call).await
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660    use churust_core::{App, Churust, TestClient};
661    use serde::{Deserialize, Serialize};
662
663    #[derive(Clone, Debug, PartialEq)]
664    struct User {
665        name: String,
666    }
667
668    fn bearer_app() -> App {
669        Churust::server()
670            .install(Auth::bearer(|token: String| async move {
671                if token == "secret" {
672                    Some(User { name: "ana".into() })
673                } else {
674                    None
675                }
676            }))
677            .routing(|r| {
678                r.get("/me", |Principal(u): Principal<User>| async move {
679                    format!("hello {}", u.name)
680                });
681            })
682            .build()
683    }
684
685    #[tokio::test]
686    async fn valid_bearer_reaches_protected_route() {
687        let client = TestClient::new(bearer_app());
688        let res = client
689            .get("/me")
690            .header("authorization", "Bearer secret")
691            .send()
692            .await;
693        assert_eq!(res.status(), StatusCode::OK);
694        assert_eq!(res.text(), "hello ana");
695    }
696
697    #[tokio::test]
698    async fn missing_token_is_401_with_challenge() {
699        let client = TestClient::new(bearer_app());
700        let res = client.get("/me").send().await;
701        assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
702        assert_eq!(res.header("www-authenticate"), Some("Bearer"));
703    }
704
705    #[tokio::test]
706    async fn wrong_token_is_401() {
707        let client = TestClient::new(bearer_app());
708        let res = client
709            .get("/me")
710            .header("authorization", "Bearer nope")
711            .send()
712            .await;
713        assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
714    }
715
716    #[tokio::test]
717    async fn basic_auth_works() {
718        let app = Churust::server()
719            .install(Auth::basic(|u: String, p: String| async move {
720                if u == "admin" && p == "pw" {
721                    Some(User { name: u })
722                } else {
723                    None
724                }
725            }))
726            .routing(|r| {
727                r.get("/me", |Principal(u): Principal<User>| async move { u.name });
728            })
729            .build();
730        let client = TestClient::new(app);
731        // base64("admin:pw") = YWRtaW46cHc=
732        let res = client
733            .get("/me")
734            .header("authorization", "Basic YWRtaW46cHc=")
735            .send()
736            .await;
737        assert_eq!(res.status(), StatusCode::OK);
738        assert_eq!(res.text(), "admin");
739    }
740
741    #[derive(Serialize, Deserialize, Clone, Debug)]
742    struct Claims {
743        sub: String,
744        exp: usize,
745    }
746
747    #[tokio::test]
748    async fn jwt_decodes_claims() {
749        use jsonwebtoken::{encode, EncodingKey, Header};
750        let secret = b"topsecret";
751        let claims = Claims {
752            sub: "u42".into(),
753            exp: 9_999_999_999,
754        };
755        let token = encode(
756            &Header::default(),
757            &claims,
758            &EncodingKey::from_secret(secret),
759        )
760        .unwrap();
761
762        let app = Churust::server()
763            .install(Auth::jwt_hs256::<Claims>(secret))
764            .routing(|r| {
765                r.get(
766                    "/who",
767                    |Principal(c): Principal<Claims>| async move { c.sub },
768                );
769            })
770            .build();
771        let client = TestClient::new(app);
772        let res = client
773            .get("/who")
774            .header("authorization", &format!("Bearer {token}"))
775            .send()
776            .await;
777        assert_eq!(res.status(), StatusCode::OK);
778        assert_eq!(res.text(), "u42");
779    }
780}