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