turul_a2a_aws_lambda/
auth.rs1use async_trait::async_trait;
4use turul_a2a::middleware::{A2aMiddleware, AuthIdentity, MiddlewareError, RequestContext};
5
6use crate::adapter::AUTHORIZER_HEADER_PREFIX;
7
8#[derive(Debug, Clone)]
10pub struct AuthorizerMapping {
11 pub owner_field: String,
13 pub include_claims: bool,
15}
16
17impl Default for AuthorizerMapping {
18 fn default() -> Self {
19 Self {
20 owner_field: "sub".into(),
21 include_claims: true,
22 }
23 }
24}
25
26pub struct LambdaAuthorizerMiddleware {
31 mapping: AuthorizerMapping,
32}
33
34impl LambdaAuthorizerMiddleware {
35 pub fn new(mapping: AuthorizerMapping) -> Self {
36 Self { mapping }
37 }
38}
39
40#[async_trait]
41impl A2aMiddleware for LambdaAuthorizerMiddleware {
42 async fn before_request(&self, ctx: &mut RequestContext) -> Result<(), MiddlewareError> {
43 let owner_header = format!("{AUTHORIZER_HEADER_PREFIX}{}", self.mapping.owner_field);
44 let owner = ctx
45 .headers
46 .get(owner_header.as_str())
47 .and_then(|v| v.to_str().ok())
48 .map(|s| s.to_string());
49
50 match owner {
51 Some(owner) if !owner.trim().is_empty() => {
52 let claims = if self.mapping.include_claims {
53 Some(collect_authorizer_claims(&ctx.headers))
54 } else {
55 None
56 };
57 ctx.identity = AuthIdentity::Authenticated { owner, claims };
58 Ok(())
59 }
60 _ => Err(MiddlewareError::Unauthenticated(
61 "Missing authorizer context".into(),
62 )),
63 }
64 }
65}
66
67fn collect_authorizer_claims(headers: &http::HeaderMap) -> serde_json::Value {
69 let mut claims = serde_json::Map::new();
70 for (key, value) in headers.iter() {
71 let key_str = key.as_str();
72 if let Some(field) = key_str.strip_prefix(AUTHORIZER_HEADER_PREFIX) {
73 if let Ok(val) = value.to_str() {
74 claims.insert(
75 field.to_string(),
76 serde_json::Value::String(val.to_string()),
77 );
78 }
79 }
80 }
81 serde_json::Value::Object(claims)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[tokio::test]
89 async fn authorizer_middleware_extracts_owner() {
90 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping::default());
91 let mut ctx = RequestContext::new();
92 ctx.headers.insert(
93 http::header::HeaderName::from_static("x-authorizer-sub"),
94 "user-123".parse().unwrap(),
95 );
96
97 mw.before_request(&mut ctx).await.unwrap();
98 assert!(ctx.identity.is_authenticated());
99 assert_eq!(ctx.identity.owner(), "user-123");
100 }
101
102 #[tokio::test]
103 async fn authorizer_middleware_custom_owner_field() {
104 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping {
105 owner_field: "userId".into(),
106 include_claims: false,
107 });
108 let mut ctx = RequestContext::new();
109 ctx.headers.insert(
110 http::header::HeaderName::from_static("x-authorizer-userid"),
111 "custom-user".parse().unwrap(),
112 );
113
114 mw.before_request(&mut ctx).await.unwrap();
115 assert_eq!(ctx.identity.owner(), "custom-user");
116 }
117
118 #[tokio::test]
119 async fn authorizer_middleware_rejects_missing_owner() {
120 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping::default());
121 let ctx = &mut RequestContext::new();
122 let err = mw.before_request(ctx).await.unwrap_err();
123 assert!(matches!(err, MiddlewareError::Unauthenticated(_)));
124 }
125
126 #[tokio::test]
127 async fn authorizer_middleware_rejects_empty_owner() {
128 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping::default());
129 let mut ctx = RequestContext::new();
130 ctx.headers.insert(
131 http::header::HeaderName::from_static("x-authorizer-sub"),
132 "".parse().unwrap(),
133 );
134 let err = mw.before_request(&mut ctx).await.unwrap_err();
135 assert!(matches!(err, MiddlewareError::Unauthenticated(_)));
136 }
137
138 #[tokio::test]
139 async fn authorizer_middleware_collects_claims() {
140 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping {
141 owner_field: "sub".into(),
142 include_claims: true,
143 });
144 let mut ctx = RequestContext::new();
145 ctx.headers.insert(
146 http::header::HeaderName::from_static("x-authorizer-sub"),
147 "user-1".parse().unwrap(),
148 );
149 ctx.headers.insert(
150 http::header::HeaderName::from_static("x-authorizer-role"),
151 "admin".parse().unwrap(),
152 );
153
154 mw.before_request(&mut ctx).await.unwrap();
155 let claims = ctx.identity.claims().unwrap();
156 assert_eq!(claims["sub"], "user-1");
157 assert_eq!(claims["role"], "admin");
158 }
159}