1use actix_web::{
2 body::{EitherBody, MessageBody},
3 dev::{Service, ServiceRequest, ServiceResponse, Transform},
4 http::{header, StatusCode},
5 Error, HttpMessage, HttpRequest, HttpResponse,
6};
7use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
8use futures::future::{ok, LocalBoxFuture, Ready};
9use hmac::{Hmac, Mac};
10use rust_zero_core::{
11 AuthFailure, JwtClaimProjection, RequestSignature, RequestSignatureVerifier,
12 AUTH_KEY_ID_HEADER, AUTH_SIGNATURE_HEADER, AUTH_TIMESTAMP_HEADER,
13};
14use serde::{de::DeserializeOwned, Serialize};
15use sha2::Sha256;
16use std::{
17 collections::BTreeMap,
18 fmt,
19 sync::Arc,
20 task::{Context, Poll},
21 time::{SystemTime, UNIX_EPOCH},
22};
23
24#[derive(Serialize)]
25struct AuthFailureBody {
26 code: &'static str,
27 message: &'static str,
28}
29
30fn auth_failure_response(failure: AuthFailure) -> HttpResponse {
31 HttpResponse::build(StatusCode::UNAUTHORIZED)
32 .insert_header((header::WWW_AUTHENTICATE, "Bearer"))
33 .json(AuthFailureBody {
34 code: failure.code(),
35 message: failure.message(),
36 })
37}
38
39type Validator<T> = dyn Fn(&str) -> Option<T> + Send + Sync;
40
41pub struct BearerAuth<T> {
43 validator: Arc<Validator<T>>,
44 challenge: header::HeaderValue,
45}
46
47impl<T> Clone for BearerAuth<T> {
48 fn clone(&self) -> Self {
49 Self {
50 validator: Arc::clone(&self.validator),
51 challenge: self.challenge.clone(),
52 }
53 }
54}
55
56impl<T> BearerAuth<T>
57where
58 T: Clone + 'static,
59{
60 pub fn new(validator: impl Fn(&str) -> Option<T> + Send + Sync + 'static) -> Self {
61 Self {
62 validator: Arc::new(validator),
63 challenge: header::HeaderValue::from_static("Bearer"),
64 }
65 }
66
67 pub fn with_realm(mut self, realm: &str) -> Result<Self, header::InvalidHeaderValue> {
68 self.challenge = header::HeaderValue::from_str(&format!("Bearer realm=\"{realm}\""))?;
69 Ok(self)
70 }
71
72 pub fn authenticated(request: &HttpRequest) -> Option<T> {
74 request
75 .extensions()
76 .get::<Authenticated<T>>()
77 .map(|identity| identity.0.clone())
78 }
79}
80
81#[derive(Debug, Clone)]
82struct Authenticated<T>(T);
83
84impl<S, B, T> Transform<S, ServiceRequest> for BearerAuth<T>
85where
86 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
87 S::Future: 'static,
88 B: MessageBody + 'static,
89 T: Clone + 'static,
90{
91 type Response = ServiceResponse<EitherBody<B>>;
92 type Error = Error;
93 type Transform = BearerAuthMiddleware<S, T>;
94 type InitError = ();
95 type Future = Ready<Result<Self::Transform, Self::InitError>>;
96
97 fn new_transform(&self, service: S) -> Self::Future {
98 ok(BearerAuthMiddleware {
99 service,
100 validator: Arc::clone(&self.validator),
101 challenge: self.challenge.clone(),
102 })
103 }
104}
105
106pub struct BearerAuthMiddleware<S, T> {
107 service: S,
108 validator: Arc<Validator<T>>,
109 challenge: header::HeaderValue,
110}
111
112impl<S, B, T> Service<ServiceRequest> for BearerAuthMiddleware<S, T>
113where
114 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
115 S::Future: 'static,
116 B: MessageBody + 'static,
117 T: Clone + 'static,
118{
119 type Response = ServiceResponse<EitherBody<B>>;
120 type Error = Error;
121 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
122
123 fn poll_ready(&self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
124 self.service.poll_ready(context)
125 }
126
127 fn call(&self, request: ServiceRequest) -> Self::Future {
128 let identity = request
129 .headers()
130 .get(header::AUTHORIZATION)
131 .and_then(|value| value.to_str().ok())
132 .and_then(bearer_token)
133 .and_then(|token| (self.validator)(token));
134
135 let Some(identity) = identity else {
136 let challenge = self.challenge.clone();
137 return Box::pin(async move {
138 let mut response = auth_failure_response(AuthFailure::InvalidCredentials);
139 response
140 .headers_mut()
141 .insert(header::WWW_AUTHENTICATE, challenge);
142 Ok(request.into_response(response.map_into_right_body()))
143 });
144 };
145
146 request.extensions_mut().insert(Authenticated(identity));
147 let future = self.service.call(request);
148 Box::pin(async move { Ok(future.await?.map_into_left_body()) })
149 }
150}
151
152pub(crate) fn bearer_token(value: &str) -> Option<&str> {
153 let (scheme, token) = value.split_once(char::is_whitespace)?;
154 let token = token.trim();
155 (scheme.eq_ignore_ascii_case("bearer")
156 && !token.is_empty()
157 && !token.contains(char::is_whitespace))
158 .then_some(token)
159}
160
161type HmacSha256 = Hmac<Sha256>;
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub enum JwtError {
166 Malformed,
167 UnsupportedAlgorithm,
168 InvalidSignature,
169 Expired,
170 NotYetValid,
171 InvalidClaims,
172}
173
174impl fmt::Display for JwtError {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter.write_str(match self {
177 Self::Malformed => "malformed JWT",
178 Self::UnsupportedAlgorithm => "JWT must use HS256",
179 Self::InvalidSignature => "invalid JWT signature",
180 Self::Expired => "JWT has expired",
181 Self::NotYetValid => "JWT is not yet valid",
182 Self::InvalidClaims => "invalid JWT claims",
183 })
184 }
185}
186
187impl std::error::Error for JwtError {}
188
189impl From<JwtError> for AuthFailure {
190 fn from(error: JwtError) -> Self {
191 match error {
192 JwtError::Malformed | JwtError::UnsupportedAlgorithm | JwtError::InvalidClaims => {
193 Self::MalformedCredentials
194 }
195 JwtError::InvalidSignature => Self::InvalidCredentials,
196 JwtError::Expired => Self::ExpiredCredentials,
197 JwtError::NotYetValid => Self::NotYetValid,
198 }
199 }
200}
201
202#[derive(Debug, Clone)]
203pub(crate) struct ProjectedClaims(pub(crate) BTreeMap<String, serde_json::Value>);
204
205pub fn encode_hs256<T>(claims: &T, secret: &[u8]) -> Result<String, JwtError>
207where
208 T: Serialize,
209{
210 if secret.is_empty() {
211 return Err(JwtError::InvalidSignature);
212 }
213 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"HS256","typ":"JWT"}"#);
214 let claims =
215 URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).map_err(|_| JwtError::InvalidClaims)?);
216 let signing_input = format!("{header}.{claims}");
217 let mut mac = HmacSha256::new_from_slice(secret).map_err(|_| JwtError::InvalidSignature)?;
218 mac.update(signing_input.as_bytes());
219 let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
220 Ok(format!("{signing_input}.{signature}"))
221}
222
223pub(crate) fn decode_hs256<T>(
224 token: &str,
225 secrets: &[Arc<[u8]>],
226 leeway_seconds: u64,
227) -> Result<T, JwtError>
228where
229 T: DeserializeOwned,
230{
231 let mut segments = token.split('.');
232 let header = segments.next().ok_or(JwtError::Malformed)?;
233 let claims = segments.next().ok_or(JwtError::Malformed)?;
234 let signature = segments.next().ok_or(JwtError::Malformed)?;
235 if segments.next().is_some() {
236 return Err(JwtError::Malformed);
237 }
238
239 let header_value: serde_json::Value = serde_json::from_slice(
240 &URL_SAFE_NO_PAD
241 .decode(header)
242 .map_err(|_| JwtError::Malformed)?,
243 )
244 .map_err(|_| JwtError::Malformed)?;
245 if header_value.get("alg").and_then(|value| value.as_str()) != Some("HS256") {
246 return Err(JwtError::UnsupportedAlgorithm);
247 }
248
249 let signature = URL_SAFE_NO_PAD
250 .decode(signature)
251 .map_err(|_| JwtError::Malformed)?;
252 let signing_input = format!("{header}.{claims}");
253 let valid = secrets.iter().any(|secret| {
254 HmacSha256::new_from_slice(secret)
255 .map(|mut mac| {
256 mac.update(signing_input.as_bytes());
257 mac.verify_slice(&signature).is_ok()
258 })
259 .unwrap_or(false)
260 });
261 if !valid {
262 return Err(JwtError::InvalidSignature);
263 }
264
265 let claim_bytes = URL_SAFE_NO_PAD
266 .decode(claims)
267 .map_err(|_| JwtError::Malformed)?;
268 let claim_value: serde_json::Value =
269 serde_json::from_slice(&claim_bytes).map_err(|_| JwtError::InvalidClaims)?;
270 let now = SystemTime::now()
271 .duration_since(UNIX_EPOCH)
272 .unwrap_or_default()
273 .as_secs();
274 if claim_value
275 .get("exp")
276 .and_then(serde_json::Value::as_u64)
277 .is_some_and(|expires| now > expires.saturating_add(leeway_seconds))
278 {
279 return Err(JwtError::Expired);
280 }
281 if claim_value
282 .get("nbf")
283 .and_then(serde_json::Value::as_u64)
284 .is_some_and(|not_before| now.saturating_add(leeway_seconds) < not_before)
285 {
286 return Err(JwtError::NotYetValid);
287 }
288
289 serde_json::from_slice(&claim_bytes).map_err(|_| JwtError::InvalidClaims)
290}
291
292pub struct JwtAuth<T> {
297 secrets: Vec<Arc<[u8]>>,
298 leeway_seconds: u64,
299 challenge: header::HeaderValue,
300 projection: JwtClaimProjection,
301 marker: std::marker::PhantomData<fn() -> T>,
302}
303
304impl<T> Clone for JwtAuth<T> {
305 fn clone(&self) -> Self {
306 Self {
307 secrets: self.secrets.clone(),
308 leeway_seconds: self.leeway_seconds,
309 challenge: self.challenge.clone(),
310 projection: self.projection.clone(),
311 marker: std::marker::PhantomData,
312 }
313 }
314}
315
316impl<T> JwtAuth<T>
317where
318 T: Clone + DeserializeOwned + 'static,
319{
320 pub fn new(secret: impl AsRef<[u8]>) -> Self {
321 let secret = secret.as_ref();
322 assert!(!secret.is_empty(), "JWT secret cannot be empty");
323 Self {
324 secrets: vec![Arc::from(secret)],
325 leeway_seconds: 0,
326 challenge: header::HeaderValue::from_static("Bearer"),
327 projection: JwtClaimProjection::default(),
328 marker: std::marker::PhantomData,
329 }
330 }
331
332 pub fn with_previous_secret(mut self, secret: impl AsRef<[u8]>) -> Self {
333 let secret = secret.as_ref();
334 assert!(!secret.is_empty(), "previous JWT secret cannot be empty");
335 self.secrets.push(Arc::from(secret));
336 self
337 }
338
339 pub fn with_leeway(mut self, seconds: u64) -> Self {
340 self.leeway_seconds = seconds;
341 self
342 }
343
344 pub fn with_claim_projection(mut self, projection: JwtClaimProjection) -> Self {
345 self.projection = projection;
346 self
347 }
348
349 pub fn claims(request: &HttpRequest) -> Option<T> {
350 request
351 .extensions()
352 .get::<JwtClaims<T>>()
353 .map(|claims| claims.0.clone())
354 }
355
356 pub fn projected_claims(request: &HttpRequest) -> Option<BTreeMap<String, serde_json::Value>> {
357 request
358 .extensions()
359 .get::<ProjectedClaims>()
360 .map(|claims| claims.0.clone())
361 }
362}
363
364#[derive(Debug, Clone)]
365pub(crate) struct JwtClaims<T>(pub(crate) T);
366
367impl<S, B, T> Transform<S, ServiceRequest> for JwtAuth<T>
368where
369 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
370 S::Future: 'static,
371 B: MessageBody + 'static,
372 T: Clone + DeserializeOwned + 'static,
373{
374 type Response = ServiceResponse<EitherBody<B>>;
375 type Error = Error;
376 type Transform = JwtAuthMiddleware<S, T>;
377 type InitError = ();
378 type Future = Ready<Result<Self::Transform, Self::InitError>>;
379
380 fn new_transform(&self, service: S) -> Self::Future {
381 ok(JwtAuthMiddleware {
382 service,
383 secrets: self.secrets.clone(),
384 leeway_seconds: self.leeway_seconds,
385 challenge: self.challenge.clone(),
386 projection: self.projection.clone(),
387 marker: std::marker::PhantomData,
388 })
389 }
390}
391
392pub struct JwtAuthMiddleware<S, T> {
393 service: S,
394 secrets: Vec<Arc<[u8]>>,
395 leeway_seconds: u64,
396 challenge: header::HeaderValue,
397 projection: JwtClaimProjection,
398 marker: std::marker::PhantomData<fn() -> T>,
399}
400
401impl<S, B, T> Service<ServiceRequest> for JwtAuthMiddleware<S, T>
402where
403 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
404 S::Future: 'static,
405 B: MessageBody + 'static,
406 T: Clone + DeserializeOwned + 'static,
407{
408 type Response = ServiceResponse<EitherBody<B>>;
409 type Error = Error;
410 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
411
412 fn poll_ready(&self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
413 self.service.poll_ready(context)
414 }
415
416 fn call(&self, request: ServiceRequest) -> Self::Future {
417 let token = request
418 .headers()
419 .get(header::AUTHORIZATION)
420 .and_then(|value| value.to_str().ok())
421 .and_then(bearer_token)
422 .ok_or(AuthFailure::MissingCredentials);
423 let result = token.and_then(|token| {
424 decode_hs256::<T>(token, &self.secrets, self.leeway_seconds).map_err(AuthFailure::from)
425 });
426
427 let claims = match result {
428 Ok(claims) => claims,
429 Err(failure) => {
430 let challenge = self.challenge.clone();
431 return Box::pin(async move {
432 let mut response = auth_failure_response(failure);
433 response
434 .headers_mut()
435 .insert(header::WWW_AUTHENTICATE, challenge);
436 Ok(request.into_response(response.map_into_right_body()))
437 });
438 }
439 };
440
441 let projected = token
442 .ok()
443 .and_then(|token| {
444 decode_hs256::<serde_json::Value>(token, &self.secrets, self.leeway_seconds).ok()
445 })
446 .map(|value| self.projection.project(&value))
447 .unwrap_or_default();
448 request.extensions_mut().insert(ProjectedClaims(projected));
449 request.extensions_mut().insert(JwtClaims(claims));
450 let future = self.service.call(request);
451 Box::pin(async move { Ok(future.await?.map_into_left_body()) })
452 }
453}
454
455#[derive(Debug, Clone)]
457pub struct RequestSignatureAuth {
458 verifier: RequestSignatureVerifier,
459}
460
461impl RequestSignatureAuth {
462 pub fn new(verifier: RequestSignatureVerifier) -> Self {
463 Self { verifier }
464 }
465
466 pub fn key_id(request: &HttpRequest) -> Option<String> {
467 request
468 .extensions()
469 .get::<SignatureKeyId>()
470 .map(|id| id.0.clone())
471 }
472}
473
474#[derive(Debug, Clone)]
475struct SignatureKeyId(String);
476
477impl<S, B> Transform<S, ServiceRequest> for RequestSignatureAuth
478where
479 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
480 S::Future: 'static,
481 B: MessageBody + 'static,
482{
483 type Response = ServiceResponse<EitherBody<B>>;
484 type Error = Error;
485 type Transform = RequestSignatureAuthMiddleware<S>;
486 type InitError = ();
487 type Future = Ready<Result<Self::Transform, Self::InitError>>;
488
489 fn new_transform(&self, service: S) -> Self::Future {
490 ok(RequestSignatureAuthMiddleware {
491 service,
492 verifier: self.verifier.clone(),
493 })
494 }
495}
496
497pub struct RequestSignatureAuthMiddleware<S> {
498 service: S,
499 verifier: RequestSignatureVerifier,
500}
501
502impl<S, B> Service<ServiceRequest> for RequestSignatureAuthMiddleware<S>
503where
504 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
505 S::Future: 'static,
506 B: MessageBody + 'static,
507{
508 type Response = ServiceResponse<EitherBody<B>>;
509 type Error = Error;
510 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
511
512 fn poll_ready(&self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
513 self.service.poll_ready(context)
514 }
515
516 fn call(&self, request: ServiceRequest) -> Self::Future {
517 let result = parse_signature(&request).and_then(|signature| {
518 self.verifier.verify(
519 &signature,
520 request.method().as_str(),
521 request
522 .uri()
523 .path_and_query()
524 .map_or(request.path(), |value| value.as_str()),
525 unix_seconds(),
526 )?;
527 Ok(signature.key_id)
528 });
529 let key_id = match result {
530 Ok(key_id) => key_id,
531 Err(failure) => {
532 return Box::pin(async move {
533 Ok(request.into_response(auth_failure_response(failure).map_into_right_body()))
534 });
535 }
536 };
537 request.extensions_mut().insert(SignatureKeyId(key_id));
538 let future = self.service.call(request);
539 Box::pin(async move { Ok(future.await?.map_into_left_body()) })
540 }
541}
542
543fn parse_signature(request: &ServiceRequest) -> Result<RequestSignature, AuthFailure> {
544 let header = |name| {
545 request
546 .headers()
547 .get(name)
548 .and_then(|value| value.to_str().ok())
549 };
550 let key_id = header(AUTH_KEY_ID_HEADER).ok_or(AuthFailure::MissingSignature)?;
551 let timestamp = header(AUTH_TIMESTAMP_HEADER)
552 .ok_or(AuthFailure::MissingSignature)?
553 .parse()
554 .map_err(|_| AuthFailure::InvalidSignature)?;
555 let signature = header(AUTH_SIGNATURE_HEADER).ok_or(AuthFailure::MissingSignature)?;
556 Ok(RequestSignature {
557 key_id: key_id.to_owned(),
558 timestamp,
559 signature: signature.to_owned(),
560 })
561}
562
563fn unix_seconds() -> i64 {
564 SystemTime::now()
565 .duration_since(UNIX_EPOCH)
566 .unwrap_or_default()
567 .as_secs() as i64
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use actix_web::{test, web, App};
574 use serde::{Deserialize, Serialize};
575
576 #[actix_rt::test]
577 async fn authenticates_and_exposes_the_identity() {
578 let app = test::init_service(
579 App::new()
580 .wrap(BearerAuth::new(|token| {
581 (token == "valid").then(|| "user-42".to_owned())
582 }))
583 .route(
584 "/",
585 web::get().to(|request: HttpRequest| async move {
586 BearerAuth::<String>::authenticated(&request).unwrap()
587 }),
588 ),
589 )
590 .await;
591
592 let request = test::TestRequest::get()
593 .uri("/")
594 .insert_header((header::AUTHORIZATION, "Bearer valid"))
595 .to_request();
596 let response = test::call_service(&app, request).await;
597
598 assert_eq!(response.status(), StatusCode::OK);
599 assert_eq!(test::read_body(response).await, "user-42");
600 }
601
602 #[actix_rt::test]
603 async fn rejects_missing_and_invalid_credentials() {
604 let app = test::init_service(
605 App::new()
606 .wrap(
607 BearerAuth::new(|token| (token == "valid").then_some(()))
608 .with_realm("api")
609 .unwrap(),
610 )
611 .route("/", web::get().to(|| async { "secret" })),
612 )
613 .await;
614
615 for authorization in [None, Some("Basic abc"), Some("Bearer invalid")] {
616 let mut request = test::TestRequest::get().uri("/");
617 if let Some(value) = authorization {
618 request = request.insert_header((header::AUTHORIZATION, value));
619 }
620 let response = test::call_service(&app, request.to_request()).await;
621 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
622 assert_eq!(
623 response.headers().get(header::WWW_AUTHENTICATE).unwrap(),
624 "Bearer realm=\"api\""
625 );
626 }
627 }
628
629 #[derive(Debug, Clone, Deserialize, Serialize)]
630 struct Claims {
631 sub: String,
632 exp: u64,
633 }
634
635 #[actix_rt::test]
636 async fn validates_jwt_claims_and_supports_secret_rotation() {
637 let claims = Claims {
638 sub: "user-42".to_owned(),
639 exp: SystemTime::now()
640 .duration_since(UNIX_EPOCH)
641 .unwrap()
642 .as_secs()
643 + 60,
644 };
645 let token = encode_hs256(&claims, b"old-secret").unwrap();
646 let app = test::init_service(
647 App::new()
648 .wrap(JwtAuth::<Claims>::new("new-secret").with_previous_secret("old-secret"))
649 .route(
650 "/",
651 web::get().to(|request: HttpRequest| async move {
652 JwtAuth::<Claims>::claims(&request).unwrap().sub
653 }),
654 ),
655 )
656 .await;
657
658 let response = test::call_service(
659 &app,
660 test::TestRequest::get()
661 .uri("/")
662 .insert_header((header::AUTHORIZATION, format!("Bearer {token}")))
663 .to_request(),
664 )
665 .await;
666 assert_eq!(response.status(), StatusCode::OK);
667 assert_eq!(test::read_body(response).await, "user-42");
668 }
669
670 #[actix_rt::test]
671 async fn rejects_expired_jwts() {
672 let token = encode_hs256(
673 &Claims {
674 sub: "user-42".to_owned(),
675 exp: 1,
676 },
677 b"secret",
678 )
679 .unwrap();
680 let app = test::init_service(
681 App::new()
682 .wrap(JwtAuth::<Claims>::new("secret"))
683 .route("/", web::get().to(|| async { "secret" })),
684 )
685 .await;
686 let response = test::call_service(
687 &app,
688 test::TestRequest::get()
689 .uri("/")
690 .insert_header((header::AUTHORIZATION, format!("Bearer {token}")))
691 .to_request(),
692 )
693 .await;
694 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
695 }
696
697 #[actix_rt::test]
698 async fn projects_selected_jwt_claims() {
699 let token = encode_hs256(
700 &Claims {
701 sub: "user-42".to_owned(),
702 exp: unix_seconds() as u64 + 60,
703 },
704 b"secret",
705 )
706 .unwrap();
707 let projection = JwtClaimProjection::new([("caller".to_owned(), "sub".to_owned())]);
708 let app = test::init_service(
709 App::new()
710 .wrap(JwtAuth::<Claims>::new("secret").with_claim_projection(projection))
711 .route(
712 "/",
713 web::get().to(|request: HttpRequest| async move {
714 web::Json(JwtAuth::<Claims>::projected_claims(&request).unwrap())
715 }),
716 ),
717 )
718 .await;
719 let response = test::call_service(
720 &app,
721 test::TestRequest::get()
722 .uri("/")
723 .insert_header((header::AUTHORIZATION, format!("Bearer {token}")))
724 .to_request(),
725 )
726 .await;
727 let projected: serde_json::Value = test::read_body_json(response).await;
728 assert_eq!(projected["caller"], "user-42");
729 assert!(projected.get("exp").is_none());
730 }
731
732 #[actix_rt::test]
733 async fn authenticates_signed_http_targets_and_reports_stable_errors() {
734 let verifier = RequestSignatureVerifier::new(
735 [("client".to_owned(), b"secret".to_vec())],
736 std::time::Duration::from_secs(30),
737 )
738 .unwrap();
739 let app = test::init_service(App::new().wrap(RequestSignatureAuth::new(verifier)).route(
740 "/jobs",
741 web::post().to(|request: HttpRequest| async move {
742 RequestSignatureAuth::key_id(&request).unwrap()
743 }),
744 ))
745 .await;
746 let signature = rust_zero_core::sign_request(
747 "client",
748 b"secret",
749 unix_seconds(),
750 "POST",
751 "/jobs?priority=high",
752 )
753 .unwrap();
754 let response = test::call_service(
755 &app,
756 test::TestRequest::post()
757 .uri("/jobs?priority=high")
758 .insert_header((AUTH_KEY_ID_HEADER, signature.key_id))
759 .insert_header((AUTH_TIMESTAMP_HEADER, signature.timestamp.to_string()))
760 .insert_header((AUTH_SIGNATURE_HEADER, signature.signature))
761 .to_request(),
762 )
763 .await;
764 assert_eq!(test::read_body(response).await, "client");
765
766 let response =
767 test::call_service(&app, test::TestRequest::post().uri("/jobs").to_request()).await;
768 let failure: serde_json::Value = test::read_body_json(response).await;
769 assert_eq!(failure["code"], "auth_missing_signature");
770 }
771}