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}