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