1use async_trait::async_trait;
56use base64::engine::general_purpose::STANDARD as BASE64;
57use base64::Engine;
58use bytes::Bytes;
59use ring::hmac;
60use std::collections::HashMap;
61use std::sync::Arc;
62use std::time::{SystemTime, UNIX_EPOCH};
63
64use crate::consumers::attach_consumer;
65use crate::context::Context;
66use crate::plugins::resources::PluginResources;
67use crate::plugins::{Plugin, PluginOutput, PluginResult};
68use crate::vars::template::Template;
69
70const HDR_ACCESS_KEY: &str = "x-hmac-access-key";
73const HDR_ALGORITHM: &str = "x-hmac-algorithm";
74const HDR_SIGNED_HEADERS: &str = "x-hmac-signed-headers";
75const HDR_SIGNATURE: &str = "x-hmac-signature";
76
77struct HmacParams {
79 access_key: String,
80 algorithm: Option<String>,
81 signature: String,
82 signed_headers: Vec<String>,
83}
84
85pub struct HmacAuthPlugin {
95 access_key: Option<String>,
97 secret_key: Option<String>,
99 algorithm: HmacAlgorithm,
101 clock_skew: u64,
104 signed_headers: Vec<String>,
106 use_consumers: bool,
108 anonymous_consumer: Option<String>,
110 keep_headers: bool,
113 hide_credentials: bool,
115 realm: Template,
119 resources: Arc<PluginResources>,
120}
121
122#[derive(Clone, Copy, PartialEq)]
124enum HmacAlgorithm {
125 Sha1,
126 Sha256,
127 Sha512,
128}
129
130impl HmacAlgorithm {
131 fn parse(name: Option<&str>) -> Result<Self, String> {
134 match name {
135 None | Some("hmac-sha256") => Ok(Self::Sha256),
136 Some("hmac-sha1") => Ok(Self::Sha1),
137 Some("hmac-sha512") => Ok(Self::Sha512),
138 Some(other) => Err(format!(
139 "Unknown hmac-auth algorithm '{}' — supported: hmac-sha1, hmac-sha256, hmac-sha512",
140 other
141 )),
142 }
143 }
144
145 fn name(&self) -> &'static str {
147 match self {
148 Self::Sha1 => "hmac-sha1",
149 Self::Sha256 => "hmac-sha256",
150 Self::Sha512 => "hmac-sha512",
151 }
152 }
153
154 fn ring_algorithm(&self) -> hmac::Algorithm {
156 match self {
157 Self::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
158 Self::Sha256 => hmac::HMAC_SHA256,
159 Self::Sha512 => hmac::HMAC_SHA512,
160 }
161 }
162}
163
164fn now() -> u64 {
166 SystemTime::now()
167 .duration_since(UNIX_EPOCH)
168 .map(|d| d.as_secs())
169 .unwrap_or(0)
170}
171
172fn parse_http_date(s: &str) -> Option<i64> {
175 let parts: Vec<&str> = s.split_whitespace().collect();
177 if parts.len() != 6 {
178 return None;
179 }
180 let day: i64 = parts[1].parse().ok()?;
181 let month = match parts[2] {
182 "Jan" => 1,
183 "Feb" => 2,
184 "Mar" => 3,
185 "Apr" => 4,
186 "May" => 5,
187 "Jun" => 6,
188 "Jul" => 7,
189 "Aug" => 8,
190 "Sep" => 9,
191 "Oct" => 10,
192 "Nov" => 11,
193 "Dec" => 12,
194 _ => return None,
195 };
196 let year: i64 = parts[3].parse().ok()?;
197 let hms: Vec<&str> = parts[4].split(':').collect();
198 if hms.len() != 3 {
199 return None;
200 }
201 let hour: i64 = hms[0].parse().ok()?;
202 let minute: i64 = hms[1].parse().ok()?;
203 let second: i64 = hms[2].parse().ok()?;
204
205 let y = if month <= 2 { year - 1 } else { year };
207 let era = if y >= 0 { y } else { y - 399 } / 400;
208 let yoe = y - era * 400;
209 let m = month;
210 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + day - 1;
211 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
212 let days = era * 146097 + doe - 719468;
213
214 Some(days * 86400 + hour * 3600 + minute * 60 + second)
215}
216
217impl HmacAuthPlugin {
218 pub fn from_config(
251 config: &HashMap<String, serde_json::Value>,
252 resources: &Arc<PluginResources>,
253 ) -> Result<Self, String> {
254 let access_key = config
255 .get("access_key")
256 .and_then(|v| v.as_str())
257 .filter(|s| !s.is_empty())
258 .map(String::from);
259
260 let secret_key = config
261 .get("secret_key")
262 .and_then(|v| v.as_str())
263 .filter(|s| !s.is_empty())
264 .map(String::from);
265
266 let use_consumers = config
267 .get("use_consumers")
268 .and_then(|v| v.as_bool())
269 .unwrap_or(false);
270
271 if access_key.is_none() && !use_consumers {
272 return Err(
273 "hmac-auth plugin requires 'access_key'+'secret_key' or 'use_consumers: true'"
274 .to_string(),
275 );
276 }
277 if access_key.is_some() && secret_key.is_none() {
278 return Err("hmac-auth plugin: 'access_key' requires 'secret_key'".to_string());
279 }
280
281 let algorithm = HmacAlgorithm::parse(config.get("algorithm").and_then(|v| v.as_str()))?;
282
283 let clock_skew = match config.get("clock_skew") {
284 None => 300,
285 Some(v) => v.as_u64().ok_or_else(|| {
286 "hmac-auth: clock_skew must be a non-negative integer".to_string()
287 })?,
288 };
289
290 let signed_headers: Vec<String> = config
291 .get("signed_headers")
292 .and_then(|v| v.as_array())
293 .map(|seq| {
294 seq.iter()
295 .filter_map(|v| v.as_str().map(|s| s.to_lowercase()))
296 .collect()
297 })
298 .unwrap_or_default();
299
300 let use_flag = |key: &str| config.get(key).and_then(|v| v.as_bool()).unwrap_or(false);
301
302 let anonymous_consumer = config
303 .get("anonymous_consumer")
304 .and_then(|v| v.as_str())
305 .map(String::from);
306
307 let realm = config
308 .get("realm")
309 .and_then(|v| v.as_str())
310 .unwrap_or("hmac")
311 .to_string();
312 let realm = Template::parse(&realm).0;
315
316 Ok(Self {
317 access_key,
318 secret_key,
319 algorithm,
320 clock_skew,
321 signed_headers,
322 use_consumers,
323 anonymous_consumer,
324 keep_headers: use_flag("keep_headers"),
325 hide_credentials: use_flag("hide_credentials"),
326 realm,
327 resources: resources.clone(),
328 })
329 }
330
331 fn reject(&self, mut ctx: Context, msg: &str) -> PluginResult {
333 let realm = self.realm.render(&ctx).into_owned();
334 ctx.response.status_code = 401;
335 ctx.response.body = Bytes::from(format!(
336 r#"{{"error": "unauthorized", "message": "{}"}}"#,
337 msg
338 ));
339 ctx.response.headers.insert(
340 "content-type".to_string(),
341 vec!["application/json".to_string()],
342 );
343 ctx.response.headers.insert(
344 "www-authenticate".to_string(),
345 vec![format!("hmac realm=\"{}\"", realm)],
346 );
347 Ok(PluginOutput::on_port(ctx, "denied"))
348 }
349
350 fn header<'a>(ctx: &'a Context, name: &str) -> Option<&'a str> {
352 ctx.request
353 .headers
354 .get(name)
355 .and_then(|v| v.first())
356 .map(|s| s.as_str())
357 }
358
359 fn retrieve_params(ctx: &Context) -> Option<HmacParams> {
362 if let Some(auth) = Self::header(ctx, "authorization") {
363 if let Some(rest) = auth.strip_prefix("Signature ") {
364 return Self::parse_authorization(rest);
365 }
366 }
367
368 let access_key = Self::header(ctx, HDR_ACCESS_KEY)?.to_string();
370 let signature = Self::header(ctx, HDR_SIGNATURE)?.to_string();
371 let algorithm = Self::header(ctx, HDR_ALGORITHM).map(String::from);
372 let signed_headers = Self::header(ctx, HDR_SIGNED_HEADERS)
373 .map(|s| s.split_whitespace().map(|h| h.to_string()).collect())
374 .unwrap_or_default();
375 Some(HmacParams {
376 access_key,
377 algorithm,
378 signature,
379 signed_headers,
380 })
381 }
382
383 fn parse_authorization(rest: &str) -> Option<HmacParams> {
386 let mut key_id = None;
387 let mut algorithm = None;
388 let mut signature = None;
389 let mut headers = Vec::new();
390
391 for field in rest.split(',') {
392 let field = field.trim();
393 let Some((k, v)) = field.split_once('=') else {
394 continue;
395 };
396 let value = v.trim().trim_matches('"');
397 match k.trim() {
398 "keyId" => key_id = Some(value.to_string()),
399 "algorithm" => algorithm = Some(value.to_string()),
400 "signature" => signature = Some(value.to_string()),
401 "headers" => headers = value.split_whitespace().map(|h| h.to_string()).collect(),
402 _ => {}
403 }
404 }
405
406 Some(HmacParams {
407 access_key: key_id?,
408 algorithm,
409 signature: signature?,
410 signed_headers: headers,
411 })
412 }
413
414 fn request_uri(ctx: &Context) -> String {
417 if ctx.request.query_params.is_empty() {
418 return ctx.request.path.clone();
419 }
420 let mut pairs: Vec<(String, String)> = Vec::new();
421 for (k, vs) in &ctx.request.query_params {
422 for v in vs {
423 pairs.push((k.clone(), v.clone()));
424 }
425 }
426 pairs.sort();
427 let query = pairs
428 .iter()
429 .map(|(k, v)| format!("{}={}", k, v))
430 .collect::<Vec<_>>()
431 .join("&");
432 format!("{}?{}", ctx.request.path, query)
433 }
434
435 fn signing_string(&self, ctx: &Context, params: &HmacParams) -> String {
437 let mut items = vec![params.access_key.clone()];
438 for h in ¶ms.signed_headers {
439 if h == "@request-target" {
440 items.push(format!("{} {}", ctx.request.method, Self::request_uri(ctx)));
441 } else if let Some(value) = Self::header(ctx, &h.to_lowercase()) {
442 items.push(format!("{}: {}", h, value));
443 }
444 }
446 let mut s = items.join("\n");
447 s.push('\n');
448 s
449 }
450
451 fn verify_signature(&self, ctx: &Context, params: &HmacParams, secret: &str) -> bool {
453 let Ok(sig_bytes) = BASE64.decode(¶ms.signature) else {
454 return false;
455 };
456 let signing_string = self.signing_string(ctx, params);
457 let key = hmac::Key::new(self.algorithm.ring_algorithm(), secret.as_bytes());
458 hmac::verify(&key, signing_string.as_bytes(), &sig_bytes).is_ok()
459 }
460
461 fn validate_common(&self, ctx: &Context, params: &HmacParams) -> Result<(), String> {
464 if let Some(ref algo) = params.algorithm {
466 if algo != self.algorithm.name() {
467 return Err("Invalid algorithm".to_string());
468 }
469 }
470
471 if self.clock_skew > 0 {
473 let date = Self::header(ctx, "date")
474 .ok_or("Date header missing, failed to validate clock skew")?;
475 let ts = parse_http_date(date).ok_or("Invalid GMT format time")?;
476 let diff = (now() as i64 - ts).unsigned_abs();
477 if diff > self.clock_skew {
478 return Err("Clock skew exceeded".to_string());
479 }
480 }
481
482 for required in &self.signed_headers {
484 if !params
485 .signed_headers
486 .iter()
487 .any(|h| h.to_lowercase() == *required)
488 {
489 return Err(format!(
490 "expected header \"{}\" missing in signing",
491 required
492 ));
493 }
494 }
495
496 Ok(())
497 }
498
499 fn strip_headers(&self, ctx: &mut Context) {
501 if !self.keep_headers {
502 ctx.request.headers.remove(HDR_ACCESS_KEY);
503 ctx.request.headers.remove(HDR_ALGORITHM);
504 ctx.request.headers.remove(HDR_SIGNED_HEADERS);
505 ctx.request.headers.remove(HDR_SIGNATURE);
506 }
507 if self.hide_credentials {
508 ctx.request.headers.remove("authorization");
509 }
510 }
511}
512
513#[async_trait]
514impl Plugin for HmacAuthPlugin {
515 fn plugin_type(&self) -> &str {
516 "hmac-auth"
517 }
518
519 async fn execute(&self, mut ctx: Context) -> PluginResult {
520 let params = match Self::retrieve_params(&ctx) {
521 Some(p) => p,
522 None => {
523 if self.anonymous_consumer.is_some() {
524 return self.attach_anonymous(ctx);
525 }
526 return self.reject(ctx, "client request can't be validated");
527 }
528 };
529
530 if let Err(e) = self.validate_common(&ctx, ¶ms) {
531 return self.reject(ctx, &e);
532 }
533
534 if let (Some(ak), Some(sk)) = (&self.access_key, &self.secret_key) {
536 if ¶ms.access_key == ak {
537 if self.verify_signature(&ctx, ¶ms, sk) {
538 self.strip_headers(&mut ctx);
539 return Ok(PluginOutput::success(ctx));
540 }
541 return self.reject(ctx, "Invalid signature");
542 }
543 }
544
545 if self.use_consumers {
547 let store = self.resources.consumers.load();
548 if let Some(consumer) = store.find_by_credential("hmac-auth", ¶ms.access_key) {
549 let secret = consumer
550 .credentials
551 .get("hmac-auth")
552 .and_then(|c| c.get("secret_key"))
553 .and_then(|v| v.as_str());
554 if let Some(secret) = secret {
555 if self.verify_signature(&ctx, ¶ms, secret) {
556 self.strip_headers(&mut ctx);
557 attach_consumer(&mut ctx, &consumer, "hmac-auth");
558 return Ok(PluginOutput::success(ctx));
559 }
560 }
561 return self.reject(ctx, "Invalid signature");
562 }
563 }
564
565 if self.anonymous_consumer.is_some() {
567 return self.attach_anonymous(ctx);
568 }
569
570 self.reject(ctx, "Invalid access key")
571 }
572}
573
574impl HmacAuthPlugin {
575 fn attach_anonymous(&self, mut ctx: Context) -> PluginResult {
577 if let Some(ref name) = self.anonymous_consumer {
578 let store = self.resources.consumers.load();
579 if let Some(consumer) = store.get(name) {
580 attach_consumer(&mut ctx, &consumer, "hmac-auth");
581 return Ok(PluginOutput::success(ctx));
582 }
583 }
584 self.reject(ctx, "Invalid user authorization")
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::consumers::{ConsumerConfig, ConsumerStore};
592 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
593
594 fn http_date(ts: i64) -> String {
597 let days = ts.div_euclid(86400);
599 let secs = ts.rem_euclid(86400);
600 let (h, m, s) = (secs / 3600, (secs % 3600) / 60, secs % 60);
601 let z = days + 719468;
603 let era = if z >= 0 { z } else { z - 146096 } / 146097;
604 let doe = z - era * 146097;
605 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
606 let y = yoe + era * 400;
607 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
608 let mp = (5 * doy + 2) / 153;
609 let d = doy - (153 * mp + 2) / 5 + 1;
610 let month = if mp < 10 { mp + 3 } else { mp - 9 };
611 let year = if month <= 2 { y + 1 } else { y };
612 let month_name = [
613 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
614 ][(month - 1) as usize];
615 format!(
617 "Mon, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
618 d, month_name, year, h, m, s
619 )
620 }
621
622 fn base_ctx() -> Context {
623 Context {
624 request: GatewayRequest {
625 method: "GET".to_string(),
626 path: "/api".to_string(),
627 host: "h".to_string(),
628 scheme: "http".to_string(),
629 headers: HashMap::new(),
630 query_params: HashMap::new(),
631 body: Bytes::new(),
632 remote_addr: "1.2.3.4:5".to_string(),
633 protocol: Protocol::Http1,
634 },
635 response: GatewayResponse {
636 status_code: 0,
637 headers: HashMap::new(),
638 body: Bytes::new(),
639 stream: None,
640 },
641 message: HashMap::new(),
642 errors: Vec::new(),
643 }
644 }
645
646 fn sign(
649 secret: &str,
650 alg: HmacAlgorithm,
651 access_key: &str,
652 signed: &[(&str, &str)],
653 method: &str,
654 uri: &str,
655 ) -> String {
656 let mut items = vec![access_key.to_string()];
657 for (h, v) in signed {
658 if *h == "@request-target" {
659 items.push(format!("{} {}", method, uri));
660 } else {
661 items.push(format!("{}: {}", h, v));
662 }
663 }
664 let mut s = items.join("\n");
665 s.push('\n');
666 let key = hmac::Key::new(alg.ring_algorithm(), secret.as_bytes());
667 BASE64.encode(hmac::sign(&key, s.as_bytes()).as_ref())
668 }
669
670 fn signed_request(secret: &str, access_key: &str, alg: HmacAlgorithm, date: &str) -> Context {
672 let mut ctx = base_ctx();
673 let signature = sign(secret, alg, access_key, &[("date", date)], "GET", "/api");
674 ctx.request
675 .headers
676 .insert("date".to_string(), vec![date.to_string()]);
677 ctx.request
678 .headers
679 .insert(HDR_ACCESS_KEY.to_string(), vec![access_key.to_string()]);
680 ctx.request
681 .headers
682 .insert(HDR_ALGORITHM.to_string(), vec![alg.name().to_string()]);
683 ctx.request
684 .headers
685 .insert(HDR_SIGNED_HEADERS.to_string(), vec!["date".to_string()]);
686 ctx.request
687 .headers
688 .insert(HDR_SIGNATURE.to_string(), vec![signature]);
689 ctx
690 }
691
692 fn inline_plugin(extra: serde_json::Value) -> HmacAuthPlugin {
693 let mut config: HashMap<String, serde_json::Value> =
694 serde_json::from_value(serde_json::json!({
695 "access_key": "ak1",
696 "secret_key": "sk1",
697 }))
698 .unwrap();
699 if let serde_json::Value::Object(m) = extra {
700 for (k, v) in m {
701 config.insert(k, v);
702 }
703 }
704 HmacAuthPlugin::from_config(&config, &PluginResources::empty()).unwrap()
705 }
706
707 #[test]
708 fn test_config_requires_credential_or_consumers() {
709 assert!(HmacAuthPlugin::from_config(&HashMap::new(), &PluginResources::empty()).is_err());
710 let cfg: HashMap<String, serde_json::Value> =
712 serde_json::from_value(serde_json::json!({ "access_key": "x" })).unwrap();
713 assert!(HmacAuthPlugin::from_config(&cfg, &PluginResources::empty()).is_err());
714 }
715
716 #[test]
717 fn test_rejects_unknown_algorithm() {
718 let cfg: HashMap<String, serde_json::Value> = serde_json::from_value(
719 serde_json::json!({ "access_key": "a", "secret_key": "b", "algorithm": "md5" }),
720 )
721 .unwrap();
722 assert!(HmacAuthPlugin::from_config(&cfg, &PluginResources::empty()).is_err());
723 }
724
725 #[test]
726 fn test_http_date_round_trip() {
727 assert_eq!(
729 parse_http_date("Sun, 09 Sep 2001 01:46:40 GMT"),
730 Some(1_000_000_000)
731 );
732 let ts = 1_600_000_000;
734 assert_eq!(parse_http_date(&http_date(ts)), Some(ts));
735 }
736
737 #[tokio::test]
738 async fn test_inline_valid_signature_passes() {
739 let plugin = inline_plugin(serde_json::json!({}));
740 let date = http_date(now() as i64);
741 let ctx = signed_request("sk1", "ak1", HmacAlgorithm::Sha256, &date);
742 let out = plugin.execute(ctx).await.unwrap();
743 assert!(!out.context.request.headers.contains_key(HDR_SIGNATURE));
745 }
746
747 #[tokio::test]
748 async fn test_wrong_secret_rejected() {
749 let plugin = inline_plugin(serde_json::json!({}));
750 let date = http_date(now() as i64);
751 let ctx = signed_request("wrong", "ak1", HmacAlgorithm::Sha256, &date);
753 let out = plugin.execute(ctx).await.unwrap();
754 assert_eq!(out.port, Some("denied"));
755 assert_eq!(out.context.response.status_code, 401);
756 }
757
758 #[tokio::test]
759 async fn test_reject_realm_renders_template() {
760 let plugin = inline_plugin(serde_json::json!({ "realm": "realm-{{request.host}}" }));
762 let date = http_date(now() as i64);
763 let mut ctx = signed_request("wrong", "ak1", HmacAlgorithm::Sha256, &date);
764 ctx.request.host = "tenant-b.example.com".to_string();
765 let out = plugin.execute(ctx).await.unwrap();
766 assert_eq!(out.port, Some("denied"));
767 assert_eq!(
768 out.context.response.headers.get("www-authenticate"),
769 Some(&vec![
770 "hmac realm=\"realm-tenant-b.example.com\"".to_string()
771 ])
772 );
773 }
774
775 #[tokio::test]
776 async fn test_clock_skew_exceeded_rejected() {
777 let plugin = inline_plugin(serde_json::json!({ "clock_skew": 10 }));
778 let stale = http_date(now() as i64 - 3600);
779 let ctx = signed_request("sk1", "ak1", HmacAlgorithm::Sha256, &stale);
780 let out = plugin.execute(ctx).await.unwrap();
781 assert_eq!(out.port, Some("denied"));
782 }
783
784 #[tokio::test]
785 async fn test_missing_required_signed_header_rejected() {
786 let plugin = inline_plugin(serde_json::json!({ "signed_headers": ["@request-target"] }));
788 let date = http_date(now() as i64);
789 let ctx = signed_request("sk1", "ak1", HmacAlgorithm::Sha256, &date);
790 let out = plugin.execute(ctx).await.unwrap();
791 assert_eq!(out.port, Some("denied"));
792 }
793
794 #[tokio::test]
795 async fn test_authorization_signature_form() {
796 let plugin = inline_plugin(serde_json::json!({ "clock_skew": 0 }));
797 let mut ctx = base_ctx();
798 let signature = sign(
799 "sk1",
800 HmacAlgorithm::Sha256,
801 "ak1",
802 &[("@request-target", "")],
803 "GET",
804 "/api",
805 );
806 let auth = format!(
807 "Signature keyId=\"ak1\",algorithm=\"hmac-sha256\",headers=\"@request-target\",signature=\"{}\"",
808 signature
809 );
810 ctx.request
811 .headers
812 .insert("authorization".to_string(), vec![auth]);
813 let out = plugin.execute(ctx).await.unwrap();
814 assert_eq!(out.port, None);
815 }
816
817 fn resources_with_consumers() -> Arc<PluginResources> {
818 let resources = PluginResources::empty();
819 let consumers: Vec<ConsumerConfig> = serde_json::from_value(serde_json::json!([
820 {
821 "name": "alice",
822 "credentials": { "hmac-auth": { "access_key": "alice-ak", "secret_key": "alice-sk" } }
823 },
824 { "name": "guest" }
825 ]))
826 .unwrap();
827 resources
828 .consumers
829 .store(Arc::new(ConsumerStore::from_config(&consumers).unwrap()));
830 resources
831 }
832
833 #[tokio::test]
834 async fn test_consumer_mode_attaches_identity() {
835 let resources = resources_with_consumers();
836 let mut config = HashMap::new();
837 config.insert("use_consumers".to_string(), serde_json::json!(true));
838 config.insert("clock_skew".to_string(), serde_json::json!(0));
839 let plugin = HmacAuthPlugin::from_config(&config, &resources).unwrap();
840
841 let date = http_date(now() as i64);
842 let ctx = signed_request("alice-sk", "alice-ak", HmacAlgorithm::Sha256, &date);
843 let out = plugin.execute(ctx).await.unwrap();
844 assert_eq!(
845 out.context.message.get("consumer.name"),
846 Some(&serde_json::json!("alice"))
847 );
848 assert_eq!(
849 out.context.request.headers.get("x-consumer-username"),
850 Some(&vec!["alice".to_string()])
851 );
852
853 let ctx = signed_request("x", "nobody", HmacAlgorithm::Sha256, &date);
855 let out = plugin.execute(ctx).await.unwrap();
856 assert_eq!(out.port, Some("denied"));
857 }
858
859 #[tokio::test]
860 async fn test_anonymous_consumer_fallback() {
861 let resources = resources_with_consumers();
862 let mut config = HashMap::new();
863 config.insert("use_consumers".to_string(), serde_json::json!(true));
864 config.insert("anonymous_consumer".to_string(), serde_json::json!("guest"));
865 let plugin = HmacAuthPlugin::from_config(&config, &resources).unwrap();
866
867 let out = plugin.execute(base_ctx()).await.unwrap();
869 assert_eq!(
870 out.context.message.get("consumer.name"),
871 Some(&serde_json::json!("guest"))
872 );
873 }
874}