1use 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#[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#[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#[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 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 pub fn with_policy(mut self, policy: AccessPolicy<R, G>) -> Self {
176 self.mode.policy = policy;
177 self
178 }
179
180 pub fn allow_anonymous_with_optional_user(mut self) -> Self {
182 self.mode.optional = true;
183 self
184 }
185
186 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 #[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 #[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 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
238impl<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 pub fn allow_anonymous_with_optional_user(mut self) -> Self {
248 self.mode.optional = true;
249 self
250 }
251}
252
253impl<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(), 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#[derive(Clone)]
302pub 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 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 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#[derive(Clone)]
494pub 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 req.extensions_mut()
589 .insert(StaticTokenAuthorized::new(true));
590
591 let fut = self.inner.call(req);
592 Box::pin(fut)
593 }
594}
595
596#[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}