a2a_agents/control_plane/
auth.rs1use axum::body::Body;
15use axum::extract::Request;
16use axum::http::{StatusCode, header};
17use axum::middleware::Next;
18use axum::response::{IntoResponse, Response};
19use axum::{Json, middleware};
20
21#[derive(Debug, Clone)]
23pub enum ControlPlaneAuth {
24 BearerToken(String),
26 Disabled,
30}
31
32impl ControlPlaneAuth {
33 pub fn bearer(token: impl Into<String>) -> Self {
35 Self::BearerToken(token.into())
36 }
37
38 pub(crate) fn apply(self, router: axum::Router) -> axum::Router {
40 match self {
41 Self::Disabled => router,
42 Self::BearerToken(token) => router.layer(middleware::from_fn(
43 move |req: Request<Body>, next: Next| {
44 let token = token.clone();
45 async move {
46 match bearer_of(&req) {
47 Some(presented) if constant_time_eq(presented, &token) => {
48 next.run(req).await
49 }
50 _ => unauthorized(),
51 }
52 }
53 },
54 )),
55 }
56 }
57}
58
59fn bearer_of(req: &Request<Body>) -> Option<&str> {
61 req.headers()
62 .get(header::AUTHORIZATION)?
63 .to_str()
64 .ok()?
65 .strip_prefix("Bearer ")
66 .map(str::trim)
67}
68
69fn constant_time_eq(a: &str, b: &str) -> bool {
74 let (a, b) = (a.as_bytes(), b.as_bytes());
75 if a.len() != b.len() {
76 return false;
77 }
78 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
79}
80
81fn unauthorized() -> Response {
83 (
84 StatusCode::UNAUTHORIZED,
85 Json(serde_json::json!({ "error": "missing or invalid bearer token" })),
86 )
87 .into_response()
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn constant_time_eq_matches_string_equality() {
96 assert!(constant_time_eq("s3cret", "s3cret"));
97 assert!(!constant_time_eq("s3cret", "s3crey"));
98 assert!(!constant_time_eq("s3cret", "s3cre"));
99 assert!(!constant_time_eq("", "x"));
100 assert!(constant_time_eq("", ""));
101 }
102
103 fn req_with_auth(header_value: &str) -> Request<Body> {
105 Request::builder()
106 .header(header::AUTHORIZATION, header_value)
107 .body(Body::empty())
108 .unwrap()
109 }
110
111 #[test]
112 fn bearer_is_parsed_only_from_a_well_formed_header() {
113 assert_eq!(bearer_of(&req_with_auth("Bearer tok")), Some("tok"));
114 assert_eq!(bearer_of(&req_with_auth("bearer tok")), None);
117 assert_eq!(bearer_of(&req_with_auth("Basic dXNlcjpwdw==")), None);
118 assert_eq!(bearer_of(&req_with_auth("tok")), None);
119 assert_eq!(
120 bearer_of(&Request::builder().body(Body::empty()).unwrap()),
121 None
122 );
123 }
124}