Skip to main content

axum_gate/gate/bearer/
mod.rs

1//! Bearer gate implementation supporting two compile-time distinct modes:
2//! - JWT bearer authentication & authorization (policy-based)
3//! - Static shared-secret bearer token (boolean authorization)
4//!
5//! Each mode exposes only the relevant builder methods at compile time:
6//!
7//! JWT Mode (BearerGate<_, _, _, JwtConfig<_, _>>):
8//!   - with_policy(...)
9//!   - require_login()            (requires R: Default; baseline role + supervisors)
10//!   - allow_anonymous_with_optional_user()
11//!   - with_static_token(token)   (transitions to static token mode)
12//!
13//! Static Token Mode (BearerGate<_, _, _, StaticTokenConfig>):
14//!   - allow_anonymous_with_optional_user()
15//!
16//! Optional Mode Semantics:
17//!   - JWT optional: always forwards; inserts:
18//!       * `Option<Account<R,G>>`
19//!       * `Option<RegisteredClaims>`
20//!   - Static token optional: always forwards; inserts:
21//!       * StaticTokenAuthorized(bool)
22//!
23//! Strict Mode Semantics:
24//!   - JWT strict: validates Authorization: Bearer `<jwt>`, enforces AccessPolicy;
25//!     inserts `Account<R,G>` and `RegisteredClaims` on success, 401 otherwise
26//!   - Static token strict: requires `Authorization: Bearer <exact_token>`;
27//!     inserts `StaticTokenAuthorized(true)` on success, 401 otherwise
28//!
29//! Example (JWT strict):
30//! ```rust
31//! use std::sync::Arc;
32//! use axum::Router;
33//! use axum_gate::prelude::*;
34//! let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
35//!
36//! let router = Router::<()>::new();
37//! let gate = Gate::bearer::<JsonWebToken::<JwtClaims<Account<Role, Group>>>, Role, Group>("my-app", codec)
38//!     .with_policy(AccessPolicy::require_role(Role::Admin));
39//! router.layer(gate);
40//! ```
41//!
42//! Example (JWT optional):
43//! ```rust
44//! use std::sync::Arc;
45//! use axum_gate::prelude::*;
46//! let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
47//!
48//! let gate = Gate::bearer::<JsonWebToken::<JwtClaims<Account<Role, Group>>>, Role, Group>("my-app", codec)
49//!     .allow_anonymous_with_optional_user(); // Option<Account>, Option<RegisteredClaims>
50//! ```
51//!
52//! Transition to static token mode (compile-time change of available methods):
53//! ```rust
54//! use std::sync::Arc;
55//! use axum_gate::prelude::*;
56//! let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
57//!
58//! let gate = Gate::bearer::<JsonWebToken::<JwtClaims<Account<Role, Group>>>, Role, Group>("svc-a", codec)
59//!     .with_static_token("shared-secret"); // now static token mode (no with_policy)
60//! ```
61//!
62//! Static token optional:
63//! ```rust
64//! use std::sync::Arc;
65//! use axum_gate::prelude::*;
66//! let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
67//!
68//! let gate = Gate::bearer::<JsonWebToken::<JwtClaims<Account<Role, Group>>>, Role, Group>("svc-a", codec)
69//!     .with_static_token("shared-secret")
70//!     .allow_anonymous_with_optional_user(); // installs StaticTokenAuthorized(bool)
71//! ```
72//!
73//! Handler extraction (static token optional):
74//! ```rust
75//! use axum_gate::gate::bearer::StaticTokenAuthorized;
76//!
77//! async fn handler(
78//!     axum::Extension(token_auth): axum::Extension<StaticTokenAuthorized>
79//! ) {
80//!     if token_auth.is_authorized() { /* privileged */ } else { /* public */ }
81//! }
82//! ```
83
84use std::convert::Infallible;
85use std::future::Future;
86use std::pin::Pin;
87use std::sync::Arc;
88
89use axum::{body::Body, extract::Request, http::Response};
90use http::StatusCode;
91use tower::{Layer, Service};
92use tracing::{debug, trace, warn};
93
94pub use self::static_token_authorized::StaticTokenAuthorized;
95use crate::accounts::Account;
96use crate::authz::{AccessHierarchy, AccessPolicy, AuthorizationService};
97use crate::codecs::Codec;
98use crate::codecs::jwt::{JwtClaims, JwtValidationResult, JwtValidationService, RegisteredClaims};
99
100mod static_token_authorized;
101
102/// JWT mode configuration (compile-time).
103#[derive(Clone)]
104pub struct JwtConfig<R, G>
105where
106    R: AccessHierarchy + Eq + std::fmt::Display,
107    G: Eq,
108{
109    policy: AccessPolicy<R, G>,
110    optional: bool,
111}
112
113impl<R, G> std::fmt::Debug for JwtConfig<R, G>
114where
115    R: AccessHierarchy + Eq + std::fmt::Display,
116    G: Eq,
117{
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("JwtConfig")
120            .field("optional", &self.optional)
121            .finish_non_exhaustive()
122    }
123}
124
125/// Static token mode configuration (compile-time).
126#[derive(Clone)]
127pub struct StaticTokenConfig {
128    token: String,
129    optional: bool,
130}
131
132impl std::fmt::Debug for StaticTokenConfig {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        f.debug_struct("StaticTokenConfig")
135            .field("token", &"<redacted>")
136            .field("optional", &self.optional)
137            .finish_non_exhaustive()
138    }
139}
140
141/// Generic bearer gate with compile-time mode parameter.
142#[derive(Clone)]
143pub struct BearerGate<C, R, G, M>
144where
145    C: Codec,
146    R: AccessHierarchy + Eq + std::fmt::Display,
147    G: Eq,
148{
149    issuer: String,
150    codec: Arc<C>,
151    mode: M,
152    _phantom: std::marker::PhantomData<(R, G)>,
153}
154
155impl<C, R, G> BearerGate<C, R, G, JwtConfig<R, G>>
156where
157    C: Codec,
158    R: AccessHierarchy + Eq + std::fmt::Display,
159    G: Eq + Clone,
160{
161    /// Internal constructor (used by `Gate::bearer`).
162    pub(crate) fn new_with_codec(issuer: &str, codec: Arc<C>) -> Self {
163        Self {
164            issuer: issuer.to_string(),
165            codec,
166            mode: JwtConfig {
167                policy: AccessPolicy::deny_all(),
168                optional: false,
169            },
170            _phantom: std::marker::PhantomData,
171        }
172    }
173
174    /// Set access policy (OR semantics between requirements).
175    pub fn with_policy(mut self, policy: AccessPolicy<R, G>) -> Self {
176        self.mode.policy = policy;
177        self
178    }
179
180    /// Turn on optional mode (install `Option<Account>`, `Option<RegisteredClaims>`).
181    pub fn allow_anonymous_with_optional_user(mut self) -> Self {
182        self.mode.optional = true;
183        self
184    }
185
186    /// Configure the gate to allow any authenticated user: the baseline role (least
187    /// privileged) from `Default::default()` and all supervisor roles as defined by
188    /// your `AccessHierarchy`.
189    ///
190    /// Equivalent to `with_policy(AccessPolicy::require_role_or_supervisor(R::default()))`.
191    /// Requires `R: Default`.
192    pub fn require_login(mut self) -> Self
193    where
194        R: Default,
195    {
196        let baseline = R::default();
197        self.mode.policy = AccessPolicy::require_role_or_supervisor(baseline);
198        self
199    }
200
201    /// Enables Prometheus metrics for audit logging (JWT bearer mode).
202    ///
203    /// No-op unless both `audit-logging` and `prometheus` features are enabled.
204    /// Safe to call multiple times; registration is idempotent.
205    #[cfg(feature = "prometheus")]
206    pub fn with_prometheus_metrics(self) -> Self {
207        let _ = crate::audit::prometheus_metrics::install_prometheus_metrics();
208        self
209    }
210
211    /// Installs Prometheus metrics into the provided registry (JWT bearer mode).
212    ///
213    /// No-op if metrics already installed. Returns `self` for builder chaining.
214    #[cfg(feature = "prometheus")]
215    pub fn with_prometheus_registry(self, registry: &prometheus::Registry) -> Self {
216        let _ =
217            crate::audit::prometheus_metrics::install_prometheus_metrics_with_registry(registry);
218        self
219    }
220
221    /// Transition to static token mode: policies are discarded.
222    pub fn with_static_token(
223        self,
224        token: impl Into<String>,
225    ) -> BearerGate<C, R, G, StaticTokenConfig> {
226        BearerGate {
227            issuer: self.issuer,
228            codec: self.codec,
229            mode: StaticTokenConfig {
230                token: token.into(),
231                optional: false,
232            },
233            _phantom: std::marker::PhantomData,
234        }
235    }
236}
237
238// (require_login specialization for crate::auth::Role/Group temporarily removed to resolve generic issues)
239
240impl<C, R, G> BearerGate<C, R, G, StaticTokenConfig>
241where
242    C: Codec,
243    R: AccessHierarchy + Eq + std::fmt::Display,
244    G: Eq + Clone,
245{
246    /// Enable optional mode (install StaticTokenAuthorized(bool)).
247    pub fn allow_anonymous_with_optional_user(mut self) -> Self {
248        self.mode.optional = true;
249        self
250    }
251}
252
253// ===================== LAYER IMPLEMENTATIONS ======================
254
255impl<S, C, R, G> Layer<S> for BearerGate<C, R, G, JwtConfig<R, G>>
256where
257    C: Codec,
258    R: AccessHierarchy + Eq + std::fmt::Display + Sync + Send + 'static,
259    G: Eq + Clone + Sync + Send + 'static,
260{
261    type Service = JwtBearerService<C, R, G, S>;
262
263    fn layer(&self, inner: S) -> Self::Service {
264        if self.mode.optional {
265            JwtBearerService::new_optional(
266                inner,
267                &self.issuer,
268                self.mode.policy.clone(), // policy unused in optional mode but cloned for uniform struct
269                Arc::clone(&self.codec),
270            )
271        } else {
272            JwtBearerService::new(
273                inner,
274                &self.issuer,
275                self.mode.policy.clone(),
276                Arc::clone(&self.codec),
277            )
278        }
279    }
280}
281
282impl<S, C, R, G> Layer<S> for BearerGate<C, R, G, StaticTokenConfig>
283where
284    C: Codec,
285    R: AccessHierarchy + Eq + std::fmt::Display,
286    G: Eq + Clone,
287{
288    type Service = StaticTokenService<S>;
289
290    fn layer(&self, inner: S) -> Self::Service {
291        if self.mode.optional {
292            StaticTokenService::new_optional(inner, self.mode.token.clone())
293        } else {
294            StaticTokenService::new(inner, self.mode.token.clone())
295        }
296    }
297}
298
299// ===================== JWT SERVICE ======================
300
301#[derive(Clone)]
302/// JWT bearer token authentication service.
303///
304/// This service handles JWT bearer token authentication for protected routes,
305/// validating tokens from the `Authorization: Bearer <token>` header.
306pub struct JwtBearerService<C, R, G, S>
307where
308    C: Codec,
309    R: AccessHierarchy + Eq + std::fmt::Display,
310    G: Eq + Clone,
311{
312    inner: S,
313    authorization: AuthorizationService<R, G>,
314    validator: JwtValidationService<C>,
315    optional: bool,
316}
317
318impl<C, R, G, S> JwtBearerService<C, R, G, S>
319where
320    C: Codec,
321    R: AccessHierarchy + Eq + std::fmt::Display,
322    G: Eq + Clone,
323{
324    fn new(inner: S, issuer: &str, policy: AccessPolicy<R, G>, codec: Arc<C>) -> Self {
325        Self {
326            inner,
327            authorization: AuthorizationService::new(policy),
328            validator: JwtValidationService::new(codec, issuer),
329            optional: false,
330        }
331    }
332
333    fn new_optional(inner: S, issuer: &str, policy: AccessPolicy<R, G>, codec: Arc<C>) -> Self {
334        // policy retained only for debugging; not used in optional path
335        Self {
336            inner,
337            authorization: AuthorizationService::new(policy),
338            validator: JwtValidationService::new(codec, issuer),
339            optional: true,
340        }
341    }
342
343    #[allow(clippy::expect_used)]
344    fn unauthorized() -> Response<Body> {
345        Response::builder()
346            .status(StatusCode::UNAUTHORIZED)
347            .header(http::header::WWW_AUTHENTICATE, "Bearer")
348            .body(Body::from("Unauthorized"))
349            .expect("static unauthorized response")
350    }
351
352    fn bearer_token(req: &Request<Body>) -> Option<&str> {
353        let value = req.headers().get(http::header::AUTHORIZATION)?;
354        let value = value.to_str().ok()?.trim();
355        let mut parts = value.split_whitespace();
356        let scheme = parts.next()?;
357        if !scheme.eq_ignore_ascii_case("Bearer") {
358            return None;
359        }
360        parts.next()
361    }
362}
363
364impl<C, R, G, S> Service<Request<Body>> for JwtBearerService<C, R, G, S>
365where
366    S: Service<Request<Body>, Response = Response<Body>, Error = Infallible> + Send + 'static,
367    S::Future: Send + 'static,
368    Account<R, G>: Clone,
369    C: Codec<Payload = JwtClaims<Account<R, G>>>,
370    R: AccessHierarchy + Eq + std::fmt::Display + Sync + Send + 'static,
371    G: Eq + Clone + Sync + Send + 'static,
372{
373    type Response = Response<Body>;
374    type Error = Infallible;
375    type Future =
376        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
377
378    fn poll_ready(
379        &mut self,
380        cx: &mut std::task::Context<'_>,
381    ) -> std::task::Poll<Result<(), Self::Error>> {
382        self.inner.poll_ready(cx)
383    }
384
385    fn call(&mut self, mut req: Request<Body>) -> Self::Future {
386        #[cfg(feature = "audit-logging")]
387        use crate::audit;
388
389        let unauthorized_future = Box::pin(async move { Ok(Self::unauthorized()) });
390
391        #[cfg(feature = "audit-logging")]
392        let _span = audit::request_span(req.method().as_str(), req.uri().path(), None);
393
394        if self.optional {
395            let mut opt_account: Option<Account<R, G>> = None;
396            let mut opt_claims: Option<RegisteredClaims> = None;
397
398            if let Some(token) = Self::bearer_token(&req) {
399                trace!("JWT optional bearer header present");
400                if let JwtValidationResult::Valid(jwt) = self.validator.validate_token(token) {
401                    // Valid JWT present; optional mode inserts only Option<...> extensions
402                    opt_account = Some(jwt.custom_claims.clone());
403                    opt_claims = Some(jwt.registered_claims.clone());
404                } else {
405                    debug!("Optional JWT: invalid token; inserting None extensions");
406                }
407            }
408
409            req.extensions_mut().insert(opt_account);
410            req.extensions_mut().insert(opt_claims);
411
412            let fut = self.inner.call(req);
413            return Box::pin(fut);
414        }
415
416        if self.authorization.policy_denies_all_access() {
417            debug!("Bearer JWT gate denying access (deny-all policy)");
418            #[cfg(feature = "audit-logging")]
419            audit::denied(None, "policy_denies_all");
420            return unauthorized_future;
421        }
422
423        let Some(token) = Self::bearer_token(&req) else {
424            #[cfg(feature = "audit-logging")]
425            audit::denied(None, "missing_authorization_header");
426            return unauthorized_future;
427        };
428
429        #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
430        let jwt_validation_start = std::time::Instant::now();
431
432        let jwt = match self.validator.validate_token(token) {
433            JwtValidationResult::Valid(jwt) => {
434                #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
435                crate::audit::prometheus_metrics::observe_jwt_validation_latency(
436                    jwt_validation_start,
437                    crate::audit::prometheus_metrics::JwtValidationOutcome::Valid,
438                );
439                jwt
440            }
441            JwtValidationResult::InvalidToken => {
442                #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
443                crate::audit::prometheus_metrics::observe_jwt_validation_latency(
444                    jwt_validation_start,
445                    crate::audit::prometheus_metrics::JwtValidationOutcome::InvalidToken,
446                );
447                debug!("JWT token validation failed");
448                #[cfg(feature = "audit-logging")]
449                audit::jwt_invalid_token("validation_failed");
450                return unauthorized_future;
451            }
452            JwtValidationResult::InvalidIssuer { expected, actual } => {
453                #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
454                crate::audit::prometheus_metrics::observe_jwt_validation_latency(
455                    jwt_validation_start,
456                    crate::audit::prometheus_metrics::JwtValidationOutcome::InvalidIssuer,
457                );
458                warn!("JWT issuer mismatch. Expected='{expected}', Actual='{actual}'");
459                #[cfg(feature = "audit-logging")]
460                audit::jwt_invalid_issuer(&expected, &actual);
461                return unauthorized_future;
462            }
463        };
464
465        #[cfg(feature = "audit-logging")]
466        let _authz_span = audit::authorization_span(Some(&jwt.custom_claims.account_id), None);
467        #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
468        let authz_start = std::time::Instant::now();
469
470        if !self.authorization.is_authorized(&jwt.custom_claims) {
471            #[cfg(feature = "audit-logging")]
472            audit::denied(Some(&jwt.custom_claims.account_id), "policy_denied");
473            #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
474            crate::audit::observe_authz_latency(authz_start, crate::audit::AuthzOutcome::Denied);
475            return unauthorized_future;
476        }
477
478        #[cfg(feature = "audit-logging")]
479        audit::authorized(&jwt.custom_claims.account_id, None);
480        #[cfg(all(feature = "audit-logging", feature = "prometheus"))]
481        crate::audit::observe_authz_latency(authz_start, crate::audit::AuthzOutcome::Authorized);
482
483        req.extensions_mut().insert(jwt.custom_claims.clone());
484        req.extensions_mut().insert(jwt.registered_claims.clone());
485
486        let fut = self.inner.call(req);
487        Box::pin(fut)
488    }
489}
490
491// ===================== STATIC TOKEN SERVICE ======================
492
493#[derive(Clone)]
494/// Static bearer token authentication service.
495///
496/// This service handles authentication using pre-configured static tokens
497/// from the `Authorization: Bearer <token>` header.
498pub struct StaticTokenService<S> {
499    inner: S,
500    token: String,
501    optional: bool,
502}
503
504impl<S> StaticTokenService<S> {
505    fn new(inner: S, token: String) -> Self {
506        Self {
507            inner,
508            token,
509            optional: false,
510        }
511    }
512
513    fn new_optional(inner: S, token: String) -> Self {
514        Self {
515            inner,
516            token,
517            optional: true,
518        }
519    }
520
521    #[allow(clippy::expect_used)]
522    fn unauthorized() -> Response<Body> {
523        Response::builder()
524            .status(StatusCode::UNAUTHORIZED)
525            .header(http::header::WWW_AUTHENTICATE, "Bearer")
526            .body(Body::from("Unauthorized"))
527            .expect("static unauthorized response")
528    }
529
530    fn bearer_token(req: &Request<Body>) -> Option<&str> {
531        let value = req.headers().get(http::header::AUTHORIZATION)?;
532        let value = value.to_str().ok()?.trim();
533        let mut parts = value.split_whitespace();
534        let scheme = parts.next()?;
535        if !scheme.eq_ignore_ascii_case("Bearer") {
536            return None;
537        }
538        parts.next()
539    }
540}
541
542impl<S> Service<Request<Body>> for StaticTokenService<S>
543where
544    S: Service<Request<Body>, Response = Response<Body>, Error = Infallible> + Send + 'static,
545    S::Future: Send + 'static,
546{
547    type Response = Response<Body>;
548    type Error = Infallible;
549    type Future =
550        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
551
552    fn poll_ready(
553        &mut self,
554        cx: &mut std::task::Context<'_>,
555    ) -> std::task::Poll<Result<(), Self::Error>> {
556        self.inner.poll_ready(cx)
557    }
558
559    fn call(&mut self, mut req: Request<Body>) -> Self::Future {
560        #[cfg(feature = "audit-logging")]
561        use crate::audit;
562
563        #[cfg(feature = "audit-logging")]
564        let _span = audit::request_span(req.method().as_str(), req.uri().path(), None);
565
566        if self.optional {
567            let provided = Self::bearer_token(&req);
568            let authorized = provided.map(|v| v == self.token).unwrap_or(false);
569            req.extensions_mut()
570                .insert(StaticTokenAuthorized::new(authorized));
571            let fut = self.inner.call(req);
572            return Box::pin(fut);
573        }
574
575        let Some(provided) = Self::bearer_token(&req) else {
576            #[cfg(feature = "audit-logging")]
577            audit::denied(None, "missing_authorization_header");
578            return Box::pin(async move { Ok(Self::unauthorized()) });
579        };
580
581        if provided != self.token {
582            #[cfg(feature = "audit-logging")]
583            audit::denied(None, "static_token_mismatch");
584            return Box::pin(async move { Ok(Self::unauthorized()) });
585        }
586
587        // Strict static token success: insert positive indicator
588        req.extensions_mut()
589            .insert(StaticTokenAuthorized::new(true));
590
591        let fut = self.inner.call(req);
592        Box::pin(fut)
593    }
594}
595
596// ===================== TESTS ======================
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use crate::accounts::Account;
602    use crate::codecs::jwt::{JsonWebToken, JwtClaims};
603    use crate::groups::Group;
604    use crate::roles::Role;
605
606    type BearerGateJsonwebtoken = BearerGate<
607        JsonWebToken<JwtClaims<Account<Role, Group>>>,
608        Role,
609        Group,
610        JwtConfig<Role, Group>,
611    >;
612
613    #[test]
614    fn jwt_gate_initial_deny_all() {
615        let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
616        let gate: BearerGateJsonwebtoken = BearerGate::new_with_codec("issuer", codec);
617        assert!(gate.mode.policy.denies_all());
618        assert!(!gate.mode.optional);
619    }
620
621    #[test]
622    fn jwt_gate_policy_set() {
623        let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
624        let gate =
625            BearerGate::new_with_codec("issuer", codec)
626                .with_policy(AccessPolicy::<Role, Group>::require_role(Role::Admin));
627        assert!(!gate.mode.policy.denies_all());
628    }
629
630    #[test]
631    fn transition_to_static_mode() {
632        let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
633        let static_gate: BearerGate<_, Role, Group, StaticTokenConfig> =
634            BearerGate::new_with_codec("issuer", codec).with_static_token("secret");
635        assert_eq!(static_gate.mode.token, "secret");
636        assert!(!static_gate.mode.optional);
637    }
638
639    #[test]
640    fn static_optional_mode() {
641        let codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
642        let static_gate: BearerGate<_, Role, Group, StaticTokenConfig> =
643            BearerGate::new_with_codec("issuer", codec)
644                .with_static_token("secret")
645                .allow_anonymous_with_optional_user();
646        assert!(static_gate.mode.optional);
647    }
648
649    #[test]
650    fn jwt_unauthorized_has_www_authenticate() {
651        tokio_test::block_on(async {
652            use axum::{body::Body, extract::Request, http::Response};
653            use std::convert::Infallible;
654            use tower::ServiceExt;
655
656            let codec =
657                std::sync::Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
658            let gate: BearerGateJsonwebtoken = BearerGate::new_with_codec("issuer", codec)
659                .with_policy(AccessPolicy::<Role, Group>::require_role(Role::Admin));
660
661            let svc = gate.layer(tower::service_fn(|_req: Request<Body>| async {
662                Ok::<_, Infallible>(Response::new(Body::from("ok")))
663            }));
664
665            let req = Request::new(Body::empty());
666            let resp = svc.oneshot(req).await.unwrap();
667
668            assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
669            let hdr = resp
670                .headers()
671                .get(http::header::WWW_AUTHENTICATE)
672                .and_then(|v| v.to_str().ok());
673            assert_eq!(hdr, Some("Bearer"));
674        });
675    }
676
677    #[test]
678    fn static_token_unauthorized_has_www_authenticate() {
679        tokio_test::block_on(async {
680            use axum::{body::Body, extract::Request, http::Response};
681            use std::convert::Infallible;
682            use tower::ServiceExt;
683
684            let codec =
685                std::sync::Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
686            let gate: BearerGate<_, Role, Group, StaticTokenConfig> =
687                BearerGate::new_with_codec("issuer", codec).with_static_token("secret");
688
689            let svc = gate.layer(tower::service_fn(|_req: Request<Body>| async {
690                Ok::<_, Infallible>(Response::new(Body::from("ok")))
691            }));
692
693            let req = Request::new(Body::empty());
694            let resp = svc.oneshot(req).await.unwrap();
695
696            assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
697            let hdr = resp
698                .headers()
699                .get(http::header::WWW_AUTHENTICATE)
700                .and_then(|v| v.to_str().ok());
701            assert_eq!(hdr, Some("Bearer"));
702        });
703    }
704}