turul_a2a_aws_lambda/
auth.rs1use async_trait::async_trait;
4use turul_a2a::middleware::{
5 A2aMiddleware, AuthFailureKind, 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 AuthFailureKind::MissingCredential,
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(
77 field.to_string(),
78 serde_json::Value::String(val.to_string()),
79 );
80 }
81 }
82 }
83 serde_json::Value::Object(claims)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[tokio::test]
91 async fn authorizer_middleware_extracts_owner() {
92 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping::default());
93 let mut ctx = RequestContext::new();
94 ctx.headers.insert(
95 http::header::HeaderName::from_static("x-authorizer-sub"),
96 "user-123".parse().unwrap(),
97 );
98
99 mw.before_request(&mut ctx).await.unwrap();
100 assert!(ctx.identity.is_authenticated());
101 assert_eq!(ctx.identity.owner(), "user-123");
102 }
103
104 #[tokio::test]
105 async fn authorizer_middleware_custom_owner_field() {
106 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping {
107 owner_field: "userId".into(),
108 include_claims: false,
109 });
110 let mut ctx = RequestContext::new();
111 ctx.headers.insert(
112 http::header::HeaderName::from_static("x-authorizer-userid"),
113 "custom-user".parse().unwrap(),
114 );
115
116 mw.before_request(&mut ctx).await.unwrap();
117 assert_eq!(ctx.identity.owner(), "custom-user");
118 }
119
120 #[tokio::test]
121 async fn authorizer_middleware_rejects_missing_owner() {
122 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping::default());
123 let ctx = &mut RequestContext::new();
124 let err = mw.before_request(ctx).await.unwrap_err();
125 assert!(matches!(err, MiddlewareError::Unauthenticated(_)));
126 }
127
128 #[tokio::test]
129 async fn authorizer_middleware_rejects_empty_owner() {
130 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping::default());
131 let mut ctx = RequestContext::new();
132 ctx.headers.insert(
133 http::header::HeaderName::from_static("x-authorizer-sub"),
134 "".parse().unwrap(),
135 );
136 let err = mw.before_request(&mut ctx).await.unwrap_err();
137 assert!(matches!(err, MiddlewareError::Unauthenticated(_)));
138 }
139
140 #[tokio::test]
141 async fn authorizer_middleware_collects_claims() {
142 let mw = LambdaAuthorizerMiddleware::new(AuthorizerMapping {
143 owner_field: "sub".into(),
144 include_claims: true,
145 });
146 let mut ctx = RequestContext::new();
147 ctx.headers.insert(
148 http::header::HeaderName::from_static("x-authorizer-sub"),
149 "user-1".parse().unwrap(),
150 );
151 ctx.headers.insert(
152 http::header::HeaderName::from_static("x-authorizer-role"),
153 "admin".parse().unwrap(),
154 );
155
156 mw.before_request(&mut ctx).await.unwrap();
157 let claims = ctx.identity.claims().unwrap();
158 assert_eq!(claims["sub"], "user-1");
159 assert_eq!(claims["role"], "admin");
160 }
161}