1pub mod forward;
60
61#[cfg(test)]
62mod tests;
63
64use std::collections::HashMap;
65use std::time::{SystemTime, UNIX_EPOCH};
66
67use hmac::{Hmac, Mac};
68use sha2::{Digest, Sha256};
69
70use crate::application::Application;
71use crate::error::{AppError, IntoResponse};
72use crate::header::Header;
73use crate::middleware::Middleware;
74use crate::request::Request;
75use crate::response::Response;
76use crate::server::ConnectionInfo;
77
78type HmacSha256 = Hmac<Sha256>;
79
80fn base64_decode(input: &str) -> Option<Vec<u8>> {
85 crate::core::base64::Decoder::EITHER.decode(input).ok()
86}
87
88fn base64url_encode(input: &[u8]) -> String {
90 const C: &[u8; 64] =
91 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
92 let mut out = String::with_capacity((input.len() + 2) / 3 * 4);
93 for chunk in input.chunks(3) {
94 let b0 = chunk[0] as usize;
95 let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
96 let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
97 out.push(C[b0 >> 2] as char);
98 out.push(C[((b0 & 3) << 4) | (b1 >> 4)] as char);
99 if chunk.len() > 1 { out.push(C[((b1 & 0xf) << 2) | (b2 >> 6)] as char); }
100 if chunk.len() > 2 { out.push(C[b2 & 0x3f] as char); }
101 }
102 out
103}
104
105#[doc(hidden)]
108pub fn base64_encode(input: &[u8]) -> String {
109 const C: &[u8; 64] =
110 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
111 let mut out = String::with_capacity((input.len() + 2) / 3 * 4);
112 for chunk in input.chunks(3) {
113 let b0 = chunk[0] as usize;
114 let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
115 let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
116 out.push(C[b0 >> 2] as char);
117 out.push(C[((b0 & 3) << 4) | (b1 >> 4)] as char);
118 out.push(if chunk.len() > 1 { C[((b1 & 0xf) << 2) | (b2 >> 6)] as char } else { '=' });
119 out.push(if chunk.len() > 2 { C[b2 & 0x3f] as char } else { '=' });
120 }
121 out
122}
123
124fn extract_string_claim(json: &str, field: &str) -> Option<String> {
127 let key = format!("\"{}\"", field);
128 let start = json.find(key.as_str())?;
129 let rest = json[start + key.len()..].trim_start();
130 let rest = rest.strip_prefix(':')?.trim_start();
131 let rest = rest.strip_prefix('"')?;
132 Some(rest[..rest.find('"')?].to_string())
133}
134
135fn extract_u64_claim(json: &str, field: &str) -> Option<u64> {
136 let key = format!("\"{}\"", field);
137 let start = json.find(key.as_str())?;
138 let rest = json[start + key.len()..].trim_start();
139 let rest = rest.strip_prefix(':')?.trim_start();
140 let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len());
141 rest[..end].parse().ok()
142}
143
144pub struct Claims {
151 pub sub: Option<String>,
153 pub exp: Option<u64>,
155 pub raw: String,
157}
158
159impl Claims {
160 fn from_json(json: String) -> Self {
161 Claims {
162 sub: extract_string_claim(&json, "sub"),
163 exp: extract_u64_claim(&json, "exp"),
164 raw: json,
165 }
166 }
167
168 pub fn is_valid_at(&self, now_secs: u64) -> bool {
171 self.exp.map_or(true, |exp| now_secs < exp)
172 }
173}
174
175fn unix_now() -> u64 {
178 SystemTime::now()
179 .duration_since(UNIX_EPOCH)
180 .unwrap_or_default()
181 .as_secs()
182}
183
184pub fn extract_bearer_token(request: &Request) -> Option<String> {
187 let h = request.header(Header::_AUTHORIZATION)?;
188 h.value.strip_prefix("Bearer ").map(str::to_string)
189}
190
191pub fn build_jwt(claims_json: &str, secret: &[u8]) -> String {
202 let header = base64url_encode(br#"{"alg":"HS256","typ":"JWT"}"#);
203 let payload = base64url_encode(claims_json.as_bytes());
204 let message = format!("{}.{}", header, payload);
205 let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key size");
206 mac.update(message.as_bytes());
207 let sig = mac.finalize().into_bytes();
208 format!("{}.{}.{}", header, payload, base64url_encode(&sig))
209}
210
211pub fn verify_jwt(token: &str, secret: &[u8]) -> Option<Claims> {
216 let mut parts = token.splitn(3, '.');
217 let header_b64 = parts.next()?;
218 let payload_b64 = parts.next()?;
219 let sig_b64 = parts.next()?;
220
221 if sig_b64.contains('.') {
222 return None; }
224
225 let header_bytes = base64_decode(header_b64)?;
227 let header_str = String::from_utf8(header_bytes).ok()?;
228 if !header_str.contains("\"HS256\"") {
229 return None;
230 }
231
232 let message = format!("{}.{}", header_b64, payload_b64);
234 let expected = base64_decode(sig_b64)?;
235 let mut mac = HmacSha256::new_from_slice(secret).ok()?;
236 mac.update(message.as_bytes());
237 mac.verify_slice(&expected).ok()?;
238
239 let payload_bytes = base64_decode(payload_b64)?;
241 let payload_str = String::from_utf8(payload_bytes).ok()?;
242 let claims = Claims::from_json(payload_str);
243
244 if !claims.is_valid_at(unix_now()) {
246 return None;
247 }
248
249 Some(claims)
250}
251
252#[cfg(feature = "auth-asymmetric")]
255fn split_jwt(token: &str) -> Option<(&str, &str, &str)> {
256 let mut parts = token.splitn(3, '.');
257 let header_b64 = parts.next()?;
258 let payload_b64 = parts.next()?;
259 let sig_b64 = parts.next()?;
260 if sig_b64.contains('.') {
261 return None; }
263 Some((header_b64, payload_b64, sig_b64))
264}
265
266#[cfg(feature = "auth-asymmetric")]
272fn finish_asymmetric_verification(header_str: &str, expected_alg: &str, payload_b64: &str) -> Option<Claims> {
273 if !header_str.contains(&format!("\"{}\"", expected_alg)) {
274 return None;
275 }
276 let payload_bytes = base64_decode(payload_b64)?;
277 let payload_str = String::from_utf8(payload_bytes).ok()?;
278 let claims = Claims::from_json(payload_str);
279 if !claims.is_valid_at(unix_now()) {
280 return None;
281 }
282 Some(claims)
283}
284
285#[cfg(feature = "auth-asymmetric")]
292pub fn verify_jwt_rs256(token: &str, public_key: &crate::rsa_key::RsaPublicKey) -> Option<Claims> {
293 let (header_b64, payload_b64, sig_b64) = split_jwt(token)?;
294 let header_str = String::from_utf8(base64_decode(header_b64)?).ok()?;
295
296 let message = format!("{}.{}", header_b64, payload_b64);
297 let signature = base64_decode(sig_b64)?;
298 if !public_key.verify_pkcs1v15_sha256(message.as_bytes(), &signature) {
299 return None;
300 }
301
302 finish_asymmetric_verification(&header_str, "RS256", payload_b64)
303}
304
305#[cfg(feature = "auth-asymmetric")]
315pub fn verify_jwt_es256(token: &str, public_key: &p256::ecdsa::VerifyingKey) -> Option<Claims> {
316 use p256::ecdsa::signature::Verifier;
317 use p256::ecdsa::Signature as EcSignature;
318
319 let (header_b64, payload_b64, sig_b64) = split_jwt(token)?;
320 let header_str = String::from_utf8(base64_decode(header_b64)?).ok()?;
321
322 let message = format!("{}.{}", header_b64, payload_b64);
323 let signature = base64_decode(sig_b64)?;
324 if signature.len() != 64 {
325 return None;
326 }
327 let sig = EcSignature::from_slice(&signature).ok()?;
328 public_key.verify(message.as_bytes(), &sig).ok()?;
329
330 finish_asymmetric_verification(&header_str, "ES256", payload_b64)
331}
332
333pub struct BasicAuthLayer<F> {
357 validate: F,
358}
359
360impl<F: Fn(&str, &str) -> bool + Send + Sync + 'static> BasicAuthLayer<F> {
361 pub fn new(validate: F) -> Self {
363 BasicAuthLayer { validate }
364 }
365}
366
367impl BasicAuthLayer<Box<dyn Fn(&str, &str) -> bool + Send + Sync>> {
368 pub fn from_htpasswd_file(path: &str) -> Result<Self, String> {
399 let paths: Vec<String> = path.split(',').map(str::trim).filter(|p| !p.is_empty()).map(str::to_string).collect();
400 if paths.is_empty() {
401 return Err("no htpasswd file given".to_string());
402 }
403 let users = HtpasswdUsers::load(&paths)?;
404 let validate: Box<dyn Fn(&str, &str) -> bool + Send + Sync> = Box::new(move |user: &str, pass: &str| users.check(user, pass));
405 Ok(BasicAuthLayer::new(validate))
406 }
407}
408
409fn verify_with_any(token: &str, secrets: &[(Option<String>, Vec<u8>)]) -> Option<Claims> {
411 let kid = token.split('.').next().and_then(base64_decode).and_then(|h| String::from_utf8(h).ok()).and_then(|h| crate::mcp::json_rpc::extract_str(&h, "kid"));
412 if let Some(kid) = kid.as_deref() {
413 if let Some((_, secret)) = secrets.iter().find(|(k, _)| k.as_deref() == Some(kid)) {
414 return verify_jwt(token, secret);
415 }
416 }
417 secrets.iter().find_map(|(_, secret)| verify_jwt(token, secret))
418}
419
420struct HtpasswdUsers {
424 paths: Vec<String>,
425 state: std::sync::Mutex<HtpasswdState>,
426}
427
428struct HtpasswdState {
429 users: HashMap<String, String>,
430 stamps: Vec<Option<(u64, SystemTime)>>,
431 checked: std::time::Instant,
432}
433
434impl HtpasswdUsers {
435 fn load(paths: &[String]) -> Result<HtpasswdUsers, String> {
436 let mut users = HashMap::new();
437 let mut stamps = Vec::new();
438 for path in paths {
439 let contents = std::fs::read_to_string(path).map_err(|e| format!("failed to read htpasswd file '{path}': {e}"))?;
440 users.extend(parse_htpasswd(&contents));
441 stamps.push(stamp(path));
442 }
443 Ok(HtpasswdUsers { paths: paths.to_vec(), state: std::sync::Mutex::new(HtpasswdState { users, stamps, checked: std::time::Instant::now() }) })
444 }
445
446 fn check(&self, user: &str, password: &str) -> bool {
447 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
448 if state.checked.elapsed() >= std::time::Duration::from_secs(1) {
449 state.checked = std::time::Instant::now();
450 let now: Vec<_> = self.paths.iter().map(|p| stamp(p)).collect();
451 if now != state.stamps {
452 let mut users = HashMap::new();
453 let mut ok = true;
454 for path in &self.paths {
455 match std::fs::read_to_string(path) {
456 Ok(contents) => users.extend(parse_htpasswd(&contents)),
457 Err(e) => {
458 eprintln!("basic auth: can't re-read '{path}': {e}; keeping the users read before");
459 ok = false;
460 break;
461 }
462 }
463 }
464 if ok {
465 state.users = users;
466 state.stamps = now;
467 }
468 }
469 }
470 state.users.get(user).is_some_and(|stored| verify_htpasswd_credential(password, stored))
471 }
472}
473
474fn stamp(path: &str) -> Option<(u64, SystemTime)> {
475 std::fs::metadata(path).ok().map(|m| (m.len(), m.modified().unwrap_or(UNIX_EPOCH)))
476}
477
478fn parse_htpasswd(contents: &str) -> HashMap<String, String> {
479 contents
480 .lines()
481 .map(str::trim)
482 .filter(|line| !line.is_empty() && !line.starts_with('#'))
483 .filter_map(|line| line.split_once(':'))
484 .map(|(user, cred)| (user.to_string(), cred.to_string()))
485 .collect()
486}
487
488fn verify_htpasswd_credential(password: &str, stored: &str) -> bool {
489 match stored.strip_prefix("{SHA256}") {
490 Some(expected_b64) => base64_encode(&Sha256::digest(password.as_bytes())) == expected_b64,
491 None => stored == password,
492 }
493}
494
495impl<F: Fn(&str, &str) -> bool + Send + Sync + 'static> Middleware for BasicAuthLayer<F> {
496 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
498 true
499 }
500
501
502 fn handle(
503 &self,
504 request: &Request,
505 connection: &ConnectionInfo,
506 next: &dyn Application,
507 ) -> Result<Response, String> {
508 let challenge = || {
509 let mut r = AppError::Unauthorized.into_response();
510 r.headers.push(Header {
511 name: "WWW-Authenticate".into(),
512 value: "Basic realm=\"Protected\"".into(),
513 });
514 r
515 };
516
517 let Some(header) = request.header(Header::_AUTHORIZATION) else {
518 return Ok(challenge());
519 };
520 let Some(encoded) = header.value.strip_prefix("Basic ") else {
521 return Ok(challenge());
522 };
523 let Some(decoded) = base64_decode(encoded) else {
524 return Ok(challenge());
525 };
526 let Ok(credentials) = String::from_utf8(decoded) else {
527 return Ok(challenge());
528 };
529 let Some((user, pass)) = credentials.split_once(':') else {
530 return Ok(challenge());
531 };
532
533 if (self.validate)(user, pass) {
534 match crate::authz::Identity::user(user, "basic") {
535 Some(identity) => crate::authz::with_identity(identity, || next.execute(request, connection)),
536 None => next.execute(request, connection),
537 }
538 } else {
539 Ok(challenge())
542 }
543 }
544}
545
546pub struct JwtLayer {
573 verifier: JwtVerifier,
574 identity_claim: String,
576}
577
578enum JwtVerifier {
579 Hs256(Vec<(Option<String>, Vec<u8>)>),
581 #[cfg(feature = "auth-asymmetric")]
582 Rs256(Box<crate::rsa_key::RsaPublicKey>),
583 #[cfg(feature = "auth-asymmetric")]
584 Es256(Box<p256::ecdsa::VerifyingKey>),
585}
586
587impl JwtLayer {
588 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
590 JwtLayer { verifier: JwtVerifier::Hs256(vec![(None, secret.into())]), identity_claim: "sub".to_string() }
591 }
592
593 pub fn secrets<I, S>(secrets: I) -> Self
597 where
598 I: IntoIterator<Item = S>,
599 S: Into<Vec<u8>>,
600 {
601 let list = secrets.into_iter().map(|s| (None, s.into())).filter(|(_, s): &(Option<String>, Vec<u8>)| !s.is_empty()).collect();
602 JwtLayer { verifier: JwtVerifier::Hs256(list), identity_claim: "sub".to_string() }
603 }
604
605 pub fn secret_with_kid(mut self, kid: impl Into<String>, secret: impl Into<Vec<u8>>) -> Self {
608 #[allow(irrefutable_let_patterns)]
610 if let JwtVerifier::Hs256(list) = &mut self.verifier {
611 list.push((Some(kid.into()), secret.into()));
612 }
613 self
614 }
615
616 #[cfg(feature = "auth-asymmetric")]
628 pub fn rs256(public_key_pem: &str) -> Result<Self, String> {
629 let key = crate::rsa_key::RsaPublicKey::from_public_key_pem(public_key_pem)
630 .map_err(|e| format!("invalid RSA public key: {e}"))?;
631 Ok(JwtLayer { verifier: JwtVerifier::Rs256(Box::new(key)), identity_claim: "sub".to_string() })
632 }
633
634 #[cfg(feature = "auth-asymmetric")]
645 pub fn es256(public_key_pem: &str) -> Result<Self, String> {
646 use p256::pkcs8::DecodePublicKey;
647 let key = p256::ecdsa::VerifyingKey::from_public_key_pem(public_key_pem)
648 .map_err(|e| format!("invalid P-256 public key: {e}"))?;
649 Ok(JwtLayer { verifier: JwtVerifier::Es256(Box::new(key)), identity_claim: "sub".to_string() })
650 }
651}
652
653impl JwtLayer {
654 pub fn identity_claim(mut self, claim: impl Into<String>) -> Self {
658 self.identity_claim = claim.into();
659 self
660 }
661}
662
663impl Middleware for JwtLayer {
664 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
666 true
667 }
668
669
670 fn handle(
671 &self,
672 request: &Request,
673 connection: &ConnectionInfo,
674 next: &dyn Application,
675 ) -> Result<Response, String> {
676 let claims = extract_bearer_token(request).and_then(|t| match &self.verifier {
677 JwtVerifier::Hs256(secrets) => verify_with_any(&t, secrets),
678 #[cfg(feature = "auth-asymmetric")]
679 JwtVerifier::Rs256(key) => verify_jwt_rs256(&t, key),
680 #[cfg(feature = "auth-asymmetric")]
681 JwtVerifier::Es256(key) => verify_jwt_es256(&t, key),
682 });
683 match claims {
684 Some(claims) => match crate::authz::Identity::from_claims(&claims.raw, &self.identity_claim, "jwt") {
685 Some(identity) => crate::authz::with_identity(identity, || next.execute(request, connection)),
686 None => next.execute(request, connection),
687 },
688 None => Ok(AppError::Unauthorized.into_response()),
689 }
690 }
691}