use axum::{
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::Response,
};
use crate::server::{AppState, constant_time_eq};
use rsclaw_config::runtime::A2aPrincipal;
#[derive(Debug, Clone)]
pub struct A2aIdentity {
pub id: String,
pub scopes: Vec<String>,
}
struct Accepted {
operator_token: Option<String>,
principals: Vec<A2aPrincipal>,
}
impl Accepted {
fn is_empty(&self) -> bool {
self.operator_token.is_none() && self.principals.is_empty()
}
fn match_secret(&self, presented: &str) -> Option<A2aIdentity> {
if let Some(t) = &self.operator_token
&& constant_time_eq(t, presented)
{
return Some(A2aIdentity {
id: "gateway-auth".to_owned(),
scopes: Vec::new(),
});
}
let mut matched: Option<&A2aPrincipal> = None;
for p in &self.principals {
if constant_time_eq(&p.secret, presented) {
matched = Some(p);
}
}
matched.map(|p| A2aIdentity {
id: p.id.clone(),
scopes: p.scopes.clone(),
})
}
}
async fn collect_accepted(state: &AppState) -> Accepted {
let gw = state.live.gateway.read().await;
Accepted {
operator_token: gw.auth_token.clone(),
principals: gw.a2a_principals.clone(),
}
}
pub async fn a2a_auth_layer(
State(state): State<AppState>,
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let accepted = collect_accepted(&state).await;
if accepted.is_empty() {
return Ok(next.run(req).await);
}
let presented = req
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.or_else(|| req.headers().get("x-api-key").and_then(|v| v.to_str().ok()));
if let Some(secret) = presented
&& let Some(identity) = accepted.match_secret(secret)
{
req.extensions_mut().insert(identity);
return Ok(next.run(req).await);
}
Err(StatusCode::UNAUTHORIZED)
}
#[cfg(test)]
mod tests {
use super::*;
fn accepted(pairs: &[(&str, &str)]) -> Accepted {
Accepted {
operator_token: None,
principals: pairs
.iter()
.map(|(id, secret)| A2aPrincipal {
id: (*id).to_owned(),
secret: (*secret).to_owned(),
scopes: Vec::new(),
})
.collect(),
}
}
#[test]
fn empty_accepted_is_dev_passthrough_marker() {
assert!(accepted(&[]).is_empty());
assert!(!accepted(&[("a", "x")]).is_empty());
assert!(
!Accepted {
operator_token: Some("t".to_owned()),
principals: vec![],
}
.is_empty()
);
}
#[test]
fn operator_token_always_wins_even_on_secret_collision() {
let a = Accepted {
operator_token: Some("master".to_owned()),
principals: vec![A2aPrincipal {
id: "alice".to_owned(),
secret: "master".to_owned(),
scopes: Vec::new(),
}],
};
assert_eq!(a.match_secret("master").unwrap().id, "gateway-auth");
let a2 = Accepted {
operator_token: Some("master".to_owned()),
principals: vec![A2aPrincipal {
id: "alice".to_owned(),
secret: "sk-alice".to_owned(),
scopes: Vec::new(),
}],
};
assert_eq!(a2.match_secret("sk-alice").unwrap().id, "alice");
}
#[test]
fn match_resolves_to_principal_identity() {
let a = accepted(&[("partner-acme", "sk-acme"), ("a1", "sk-a1")]);
assert_eq!(a.match_secret("sk-acme").unwrap().id, "partner-acme");
assert_eq!(a.match_secret("sk-a1").unwrap().id, "a1");
assert!(a.match_secret("sk-nope").is_none());
}
#[test]
fn match_is_constant_time_length_safe() {
let a = accepted(&[("a", "right")]);
assert!(a.match_secret("right").is_some());
assert!(a.match_secret("wrong").is_none());
assert!(a.match_secret("rightandmore").is_none());
}
#[test]
fn one_secret_matches_regardless_of_header() {
let a = accepted(&[("client", "shared-secret")]);
assert_eq!(a.match_secret("shared-secret").unwrap().id, "client");
}
}