1pub mod authz;
15#[cfg(feature = "builtin_jwt")]
16mod cache;
17#[cfg(feature = "builtin_jwt")]
18pub mod crypto;
19pub mod forward;
20#[cfg(feature = "builtin_jwt")]
21pub mod jwks;
22pub mod policy;
23#[cfg(feature = "builtin_jwt")]
24mod verifier;
25
26#[cfg(test)]
27mod tests;
28
29use std::collections::HashMap;
30use std::collections::HashSet;
31use std::sync::Arc;
32
33use axum::extract::State;
34use axum::http::header::{HeaderName, HeaderValue};
35use axum::http::HeaderMap;
36use axum::middleware::Next;
37use axum::response::Response;
38use serde_json::Value;
39
40use crate::config::{default_roles_claim, AuthConfig};
41use crate::hooks::TokenVerifier;
42use policy::Policies;
43
44enum Verifier {
46 #[cfg(feature = "builtin_jwt")]
52 Builtin(Box<verifier::ConfigVerifier>, Option<cache::ClaimsCache>),
53 Injected(Arc<dyn TokenVerifier>),
55}
56
57pub struct Auth {
60 verifier: Verifier,
61 claims_headers: HashMap<String, String>,
62 roles_claim: String,
63 policies: Policies,
64 #[cfg(all(test, feature = "builtin_jwt"))]
67 builtin_verifications: std::sync::atomic::AtomicUsize,
68}
69
70impl Auth {
71 pub fn build(
84 config: &AuthConfig,
85 verifier: Option<Arc<dyn TokenVerifier>>,
86 ) -> Result<Option<Arc<Self>>, String> {
87 if config.mode != "jwt" {
88 return Ok(None);
89 }
90
91 let verifier = match verifier {
92 Some(v) => {
93 if let Some(jwt) = &config.jwt {
94 if jwt.jwks_uri.is_some() || jwt.public_key_pem_file.is_some() {
95 tracing::warn!(
96 "auth.jwt names a key source, but an injected TokenVerifier is in use; \
97 the configured keys are ignored"
98 );
99 }
100 }
101 Verifier::Injected(v)
102 }
103 None => builtin_verifier(config)?,
104 };
105
106 let policies = match &config.forward_auth {
107 Some(fa) => Policies::compile(&fa.policies)?,
108 None => Policies::default(),
109 };
110
111 let (claims_headers, roles_claim) = match &config.jwt {
115 Some(jwt) => (jwt.claims_headers.clone(), jwt.roles_claim.clone()),
116 None => (HashMap::new(), default_roles_claim()),
117 };
118
119 Ok(Some(Arc::new(Self {
120 verifier,
121 claims_headers,
122 roles_claim,
123 policies,
124 #[cfg(all(test, feature = "builtin_jwt"))]
125 builtin_verifications: std::sync::atomic::AtomicUsize::new(0),
126 })))
127 }
128
129 async fn verify(&self, token: &str) -> Option<Arc<Value>> {
132 match &self.verifier {
133 #[cfg(feature = "builtin_jwt")]
134 Verifier::Builtin(v, Some(cache)) if cache.admits(token) => {
135 let key = cache::token_key(token);
136 let now = cache.now();
137 if let Some(claims) = cache.get(&key, now) {
138 return Some(claims);
139 }
140 let claims = Arc::new(self.verify_builtin(v, token).await?);
141 cache.insert(key, &claims, now);
142 Some(claims)
143 }
144 #[cfg(feature = "builtin_jwt")]
145 Verifier::Builtin(v, _) => self.verify_builtin(v, token).await.map(Arc::new),
146 Verifier::Injected(v) => v.verify(token).await.map(Arc::new),
147 }
148 }
149
150 #[cfg(feature = "builtin_jwt")]
151 async fn verify_builtin(&self, v: &verifier::ConfigVerifier, token: &str) -> Option<Value> {
152 #[cfg(test)]
153 self.builtin_verifications
154 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
155 v.verify(token).await
156 }
157
158 #[cfg(all(test, feature = "builtin_jwt"))]
160 fn cached_tokens(&self) -> usize {
161 match &self.verifier {
162 Verifier::Builtin(_, Some(cache)) => cache.len(),
163 _ => 0,
164 }
165 }
166}
167
168#[cfg(feature = "builtin_jwt")]
170fn builtin_verifier(config: &AuthConfig) -> Result<Verifier, String> {
171 let jwt = config
172 .jwt
173 .as_ref()
174 .ok_or("auth.mode is \"jwt\" but auth.jwt is not set")?;
175 Ok(Verifier::Builtin(
176 Box::new(verifier::ConfigVerifier::build(jwt)?),
177 cache::ClaimsCache::build(&jwt.cache)?,
178 ))
179}
180
181#[cfg(not(feature = "builtin_jwt"))]
184fn builtin_verifier(_config: &AuthConfig) -> Result<Verifier, String> {
185 Err(
186 "auth.mode is \"jwt\" but this build has no JWT crypto backend: enable the \
187 `rust_crypto` or `aws_lc_rs` feature, or inject a verifier with \
188 ProxyServer::with_token_verifier"
189 .to_string(),
190 )
191}
192
193pub(crate) enum AuthDecision {
195 Allow(HeaderMap, Option<Arc<Value>>),
199 Unauthenticated(&'static str),
201 Forbidden(&'static str),
203}
204
205#[derive(Clone)]
210pub(crate) struct ValidatedClaims(pub(crate) std::sync::Arc<Value>);
211
212impl Auth {
213 pub(crate) async fn decide(
217 &self,
218 headers: &HeaderMap,
219 path: &str,
220 method: &str,
221 ) -> AuthDecision {
222 let claims = match bearer_token(headers) {
224 Some(token) => match self.verify(token).await {
225 Some(c) => Some(c),
226 None => return AuthDecision::Unauthenticated("invalid or expired token"),
227 },
228 None => None,
229 };
230
231 if let Some(policy) = self.policies.match_rule(path, method) {
232 if policy.require_auth && claims.is_none() {
233 return AuthDecision::Unauthenticated("authentication required");
234 }
235 if !policy.required_roles.is_empty() {
236 let Some(claims) = claims.as_ref() else {
239 return AuthDecision::Unauthenticated("authentication required");
240 };
241 let roles = extract_roles(claims, &self.roles_claim);
242 if !policy.required_roles.iter().all(|r| roles.contains(r)) {
243 return AuthDecision::Forbidden("insufficient role");
244 }
245 }
246 }
247
248 let mut claim_headers = HeaderMap::new();
249 if let Some(claims) = &claims {
250 inject_claim_headers(&mut claim_headers, claims, &self.claims_headers);
251 }
252 AuthDecision::Allow(claim_headers, claims)
253 }
254}
255
256pub async fn middleware(
258 State(auth): State<Arc<Auth>>,
259 mut request: axum::extract::Request,
260 next: Next,
261) -> Response {
262 let path = request.uri().path().to_string();
263 let method = request.method().as_str().to_ascii_uppercase();
264
265 strip_claim_headers(request.headers_mut(), &auth.claims_headers);
269
270 match auth.decide(request.headers(), &path, &method).await {
271 AuthDecision::Unauthenticated(msg) => unauthorized(msg),
272 AuthDecision::Forbidden(msg) => forbidden(msg),
273 AuthDecision::Allow(claim_headers, claims) => {
274 let dst = request.headers_mut();
275 for (name, value) in &claim_headers {
276 dst.insert(name.clone(), value.clone());
277 }
278 if let Some(claims) = claims {
281 request.extensions_mut().insert(ValidatedClaims(claims));
282 }
283 next.run(request).await
284 }
285 }
286}
287
288fn bearer_token(headers: &HeaderMap) -> Option<&str> {
293 let value = headers.get("authorization")?.to_str().ok()?;
294 let token = value
295 .strip_prefix("Bearer ")
296 .or_else(|| value.strip_prefix("bearer "))?;
297 let token = token.trim();
298 (!token.is_empty()).then_some(token)
299}
300
301fn claim_at<'a>(claims: &'a Value, path: &str) -> Option<&'a Value> {
303 let mut cur = claims;
304 for seg in path.split('.') {
305 cur = cur.get(seg)?;
306 }
307 Some(cur)
308}
309
310fn extract_roles(claims: &Value, roles_claim: &str) -> HashSet<String> {
312 claim_at(claims, roles_claim)
313 .and_then(Value::as_array)
314 .map(|arr| {
315 arr.iter()
316 .filter_map(|v| v.as_str().map(str::to_string))
317 .collect()
318 })
319 .unwrap_or_default()
320}
321
322fn strip_claim_headers(headers: &mut HeaderMap, mapping: &HashMap<String, String>) {
325 for header in mapping.values() {
326 if let Ok(name) = HeaderName::try_from(header.as_str()) {
327 while headers.remove(&name).is_some() {}
328 }
329 }
330}
331
332fn inject_claim_headers(
334 headers: &mut HeaderMap,
335 claims: &Value,
336 mapping: &HashMap<String, String>,
337) {
338 for (claim, header) in mapping {
339 let Some(value) = claim_at(claims, claim) else {
340 continue;
341 };
342 let rendered = match value {
343 Value::String(s) => s.clone(),
344 Value::Number(n) => n.to_string(),
345 Value::Bool(b) => b.to_string(),
346 _ => continue,
348 };
349 if let (Ok(name), Ok(val)) = (
350 HeaderName::try_from(header.as_str()),
351 HeaderValue::try_from(rendered),
352 ) {
353 headers.insert(name, val);
354 }
355 }
356}
357
358fn unauthorized(message: &'static str) -> Response {
359 crate::guard::reject(tonic::Code::Unauthenticated, message)
360}
361
362fn forbidden(message: &'static str) -> Response {
363 crate::guard::reject(tonic::Code::PermissionDenied, message)
364}