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(
85 config: &AuthConfig,
86 verifier: Option<Arc<dyn TokenVerifier>>,
87 ) -> Result<Option<Arc<Self>>, String> {
88 if config.mode != "jwt" {
89 return Ok(None);
90 }
91
92 let verifier = match verifier {
93 Some(v) => {
94 if let Some(jwt) = &config.jwt {
95 if jwt.jwks_uri.is_some() || jwt.public_key_pem_file.is_some() {
96 tracing::warn!(
97 "auth.jwt names a key source, but an injected TokenVerifier is in use; \
98 the configured keys are ignored"
99 );
100 }
101 }
102 Verifier::Injected(v)
103 }
104 None => builtin_verifier(config)?,
105 };
106
107 let policies = match &config.forward_auth {
108 Some(fa) => Policies::compile(&fa.policies)?,
109 None => Policies::default(),
110 };
111
112 let (claims_headers, roles_claim) = match &config.jwt {
116 Some(jwt) => (jwt.claims_headers.clone(), jwt.roles_claim.clone()),
117 None => (HashMap::new(), default_roles_claim()),
118 };
119 if let Some(header) = claims_headers
120 .values()
121 .find(|header| crate::client_address::owns(header))
122 {
123 return Err(format!(
124 "auth.jwt.claims_headers maps a claim onto {header:?}, which the proxy \
125 sets from the resolved client address"
126 ));
127 }
128
129 Ok(Some(Arc::new(Self {
130 verifier,
131 claims_headers,
132 roles_claim,
133 policies,
134 #[cfg(all(test, feature = "builtin_jwt"))]
135 builtin_verifications: std::sync::atomic::AtomicUsize::new(0),
136 })))
137 }
138
139 async fn verify(&self, token: &str) -> Option<Arc<Value>> {
142 match &self.verifier {
143 #[cfg(feature = "builtin_jwt")]
144 Verifier::Builtin(v, Some(cache)) if cache.admits(token) => {
145 let key = cache::token_key(token);
146 let now = cache.now();
147 if let Some(claims) = cache.get(&key, now) {
148 return Some(claims);
149 }
150 let claims = Arc::new(self.verify_builtin(v, token).await?);
151 cache.insert(key, &claims, now);
152 Some(claims)
153 }
154 #[cfg(feature = "builtin_jwt")]
155 Verifier::Builtin(v, _) => self.verify_builtin(v, token).await.map(Arc::new),
156 Verifier::Injected(v) => v.verify(token).await.map(Arc::new),
157 }
158 }
159
160 #[cfg(feature = "builtin_jwt")]
161 async fn verify_builtin(&self, v: &verifier::ConfigVerifier, token: &str) -> Option<Value> {
162 #[cfg(test)]
163 self.builtin_verifications
164 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
165 v.verify(token).await
166 }
167
168 #[cfg(all(test, feature = "builtin_jwt"))]
170 fn cached_tokens(&self) -> usize {
171 match &self.verifier {
172 Verifier::Builtin(_, Some(cache)) => cache.len(),
173 _ => 0,
174 }
175 }
176}
177
178#[cfg(feature = "builtin_jwt")]
180fn builtin_verifier(config: &AuthConfig) -> Result<Verifier, String> {
181 let jwt = config
182 .jwt
183 .as_ref()
184 .ok_or("auth.mode is \"jwt\" but auth.jwt is not set")?;
185 Ok(Verifier::Builtin(
186 Box::new(verifier::ConfigVerifier::build(jwt)?),
187 cache::ClaimsCache::build(&jwt.cache)?,
188 ))
189}
190
191#[cfg(not(feature = "builtin_jwt"))]
194fn builtin_verifier(_config: &AuthConfig) -> Result<Verifier, String> {
195 Err(
196 "auth.mode is \"jwt\" but this build has no JWT crypto backend: enable the \
197 `rust_crypto` or `aws_lc_rs` feature, or inject a verifier with \
198 ProxyServer::with_token_verifier"
199 .to_string(),
200 )
201}
202
203pub(crate) enum AuthDecision {
205 Allow(HeaderMap, Option<Arc<Value>>),
209 Unauthenticated(&'static str),
211 Forbidden(&'static str),
213}
214
215#[derive(Clone)]
220pub(crate) struct ValidatedClaims(pub(crate) std::sync::Arc<Value>);
221
222impl Auth {
223 pub(crate) async fn decide(
227 &self,
228 headers: &HeaderMap,
229 path: &str,
230 method: &str,
231 ) -> AuthDecision {
232 let claims = match bearer_token(headers) {
234 Some(token) => match self.verify(token).await {
235 Some(c) => Some(c),
236 None => return AuthDecision::Unauthenticated("invalid or expired token"),
237 },
238 None => None,
239 };
240
241 if let Some(policy) = self.policies.match_rule(path, method) {
242 if policy.require_auth && claims.is_none() {
243 return AuthDecision::Unauthenticated("authentication required");
244 }
245 if !policy.required_roles.is_empty() {
246 let Some(claims) = claims.as_ref() else {
249 return AuthDecision::Unauthenticated("authentication required");
250 };
251 let roles = extract_roles(claims, &self.roles_claim);
252 if !policy.required_roles.iter().all(|r| roles.contains(r)) {
253 return AuthDecision::Forbidden("insufficient role");
254 }
255 }
256 }
257
258 let mut claim_headers = HeaderMap::new();
259 if let Some(claims) = &claims {
260 inject_claim_headers(&mut claim_headers, claims, &self.claims_headers);
261 }
262 AuthDecision::Allow(claim_headers, claims)
263 }
264}
265
266pub async fn middleware(
268 State(auth): State<Arc<Auth>>,
269 mut request: axum::extract::Request,
270 next: Next,
271) -> Response {
272 let path = request.uri().path().to_string();
273 let method = request.method().as_str().to_ascii_uppercase();
274
275 strip_claim_headers(request.headers_mut(), &auth.claims_headers);
279
280 match auth.decide(request.headers(), &path, &method).await {
281 AuthDecision::Unauthenticated(msg) => unauthorized(msg),
282 AuthDecision::Forbidden(msg) => forbidden(msg),
283 AuthDecision::Allow(claim_headers, claims) => {
284 let dst = request.headers_mut();
285 for (name, value) in &claim_headers {
286 dst.insert(name.clone(), value.clone());
287 }
288 if let Some(claims) = claims {
291 request.extensions_mut().insert(ValidatedClaims(claims));
292 }
293 next.run(request).await
294 }
295 }
296}
297
298fn bearer_token(headers: &HeaderMap) -> Option<&str> {
303 let value = headers.get("authorization")?.to_str().ok()?;
304 let token = value
305 .strip_prefix("Bearer ")
306 .or_else(|| value.strip_prefix("bearer "))?;
307 let token = token.trim();
308 (!token.is_empty()).then_some(token)
309}
310
311fn claim_at<'a>(claims: &'a Value, path: &str) -> Option<&'a Value> {
313 let mut cur = claims;
314 for seg in path.split('.') {
315 cur = cur.get(seg)?;
316 }
317 Some(cur)
318}
319
320fn extract_roles(claims: &Value, roles_claim: &str) -> HashSet<String> {
322 claim_at(claims, roles_claim)
323 .and_then(Value::as_array)
324 .map(|arr| {
325 arr.iter()
326 .filter_map(|v| v.as_str().map(str::to_string))
327 .collect()
328 })
329 .unwrap_or_default()
330}
331
332fn strip_claim_headers(headers: &mut HeaderMap, mapping: &HashMap<String, String>) {
335 for header in mapping.values() {
336 if let Ok(name) = HeaderName::try_from(header.as_str()) {
337 while headers.remove(&name).is_some() {}
338 }
339 }
340}
341
342fn inject_claim_headers(
344 headers: &mut HeaderMap,
345 claims: &Value,
346 mapping: &HashMap<String, String>,
347) {
348 for (claim, header) in mapping {
349 let Some(value) = claim_at(claims, claim) else {
350 continue;
351 };
352 let rendered = match value {
353 Value::String(s) => s.clone(),
354 Value::Number(n) => n.to_string(),
355 Value::Bool(b) => b.to_string(),
356 _ => continue,
358 };
359 if let (Ok(name), Ok(val)) = (
360 HeaderName::try_from(header.as_str()),
361 HeaderValue::try_from(rendered),
362 ) {
363 headers.insert(name, val);
364 }
365 }
366}
367
368fn unauthorized(message: &'static str) -> Response {
369 crate::guard::reject(tonic::Code::Unauthenticated, message)
370}
371
372fn forbidden(message: &'static str) -> Response {
373 crate::guard::reject(tonic::Code::PermissionDenied, message)
374}