1use std::collections::BTreeMap;
47use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
48use std::sync::{Arc, RwLock};
49
50use aws_lc_rs::signature::{
51 self, EcdsaVerificationAlgorithm, RsaParameters, RsaPublicKeyComponents, UnparsedPublicKey,
52};
53use beck_core::clock::Clock;
54use beck_core::net::{Outbound, Request};
55
56use crate::identity::{Actor, Identity, Rejected};
57
58pub const REFRESH_EVERY_MS: i64 = 5 * 60 * 1_000;
66
67pub const REFETCH_FLOOR_MS: i64 = 10_000;
73
74pub const CLOCK_SKEW_MS: i64 = 60_000;
82
83pub const LOGIN_WINDOW_MS: i64 = 10 * 60 * 1_000;
85
86const PROTOCOL_CLAIMS: &[&str] = &[
92 "aud", "azp", "at_hash", "c_hash", "exp", "iat", "iss", "jti", "nbf", "nonce", "typ",
93];
94
95#[derive(Clone, Debug)]
101pub struct Config {
102 pub issuer: String,
105 pub client_id: String,
106 pub client_secret: Option<String>,
109 pub redirect_uri: String,
112 pub scopes: String,
114 pub actor_claim: String,
117 in_cluster: bool,
129}
130
131impl Config {
132 pub fn new(issuer: &str, client_id: &str, redirect_uri: &str) -> Config {
134 Config {
135 issuer: issuer.trim_end_matches('/').to_string(),
136 client_id: client_id.to_string(),
137 client_secret: None,
138 redirect_uri: redirect_uri.to_string(),
139 scopes: "openid profile email".to_string(),
140 actor_claim: "sub".to_string(),
141 in_cluster: false,
142 }
143 }
144
145 pub fn in_cluster(issuer: &str, client_id: &str, redirect_uri: &str) -> Config {
151 Config {
152 in_cluster: true,
153 ..Config::new(issuer, client_id, redirect_uri)
154 }
155 }
156}
157
158#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct Provider {
161 pub issuer: String,
162 pub authorization_endpoint: String,
163 pub token_endpoint: String,
164 pub jwks_uri: String,
165}
166
167#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct Refusal {
179 pub client: Rejected,
180 pub why: String,
181}
182
183impl Refusal {
184 fn invalid(why: impl Into<String>) -> Refusal {
185 Refusal {
186 client: Rejected::Invalid,
187 why: why.into(),
188 }
189 }
190
191 fn expired(why: impl Into<String>) -> Refusal {
192 Refusal {
193 client: Rejected::Expired,
194 why: why.into(),
195 }
196 }
197}
198
199#[derive(Debug, Default)]
200struct KeySet {
201 keys: Vec<Key>,
202 fetched_at_millis: i64,
203}
204
205#[derive(Debug)]
207pub struct RelyingParty {
208 config: Config,
209 clock: Arc<dyn Clock>,
210 http: Arc<dyn Outbound>,
211 provider: RwLock<Option<Provider>>,
212 keys: RwLock<KeySet>,
213 stale: AtomicBool,
216 last_attempt_millis: AtomicI64,
218 seal: [u8; 32],
221}
222
223impl RelyingParty {
224 pub fn new(config: Config, clock: Arc<dyn Clock>, http: Arc<dyn Outbound>) -> RelyingParty {
225 let mut seal = [0u8; 32];
226 aws_lc_rs::rand::fill(&mut seal).expect("the system random source answers");
227 RelyingParty {
228 config,
229 clock,
230 http,
231 provider: RwLock::new(None),
232 keys: RwLock::new(KeySet::default()),
233 stale: AtomicBool::new(false),
234 last_attempt_millis: AtomicI64::new(i64::MIN),
235 seal,
236 }
237 }
238
239 pub fn config(&self) -> &Config {
240 &self.config
241 }
242
243 pub fn provider(&self) -> Option<Provider> {
245 self.provider.read().expect("not poisoned").clone()
246 }
247
248 pub fn key_count(&self) -> usize {
249 self.keys.read().expect("not poisoned").keys.len()
250 }
251
252 pub fn refresh(&self) -> Result<(), String> {
258 let now = self.clock.now_millis();
259 self.last_attempt_millis.store(now, Ordering::Relaxed);
260 let provider = match self.provider() {
261 Some(p) => p,
262 None => {
263 let p = self.discover()?;
264 *self.provider.write().expect("not poisoned") = Some(p.clone());
265 p
266 }
267 };
268 let body = self.get(&provider.jwks_uri)?;
269 let keys = parse_jwks(&body)?;
270 if keys.is_empty() {
271 return Err(format!(
272 "`{}` published a key set with no key this relying party can use",
273 provider.jwks_uri
274 ));
275 }
276 *self.keys.write().expect("not poisoned") = KeySet {
277 keys,
278 fetched_at_millis: now,
279 };
280 self.stale.store(false, Ordering::Relaxed);
281 Ok(())
282 }
283
284 pub fn refresh_due(&self) -> bool {
287 let now = self.clock.now_millis();
288 let last = self.last_attempt_millis.load(Ordering::Relaxed);
289 if now.saturating_sub(last) < REFETCH_FLOOR_MS {
290 return false;
291 }
292 self.stale.load(Ordering::Relaxed)
293 || now.saturating_sub(self.keys.read().expect("not poisoned").fetched_at_millis)
294 >= REFRESH_EVERY_MS
295 }
296
297 fn discover(&self) -> Result<Provider, String> {
298 let url = format!("{}/.well-known/openid-configuration", self.config.issuer);
299 let body = self.get(&url)?;
300 let doc: serde_json::Value =
301 serde_json::from_str(&body).map_err(|e| format!("`{url}` is not JSON: {e}"))?;
302 let field = |name: &str| -> Result<String, String> {
303 doc.get(name)
304 .and_then(|v| v.as_str())
305 .map(|s| s.to_string())
306 .ok_or_else(|| format!("`{url}` has no `{name}`"))
307 };
308 let provider = Provider {
309 issuer: field("issuer")?,
310 authorization_endpoint: field("authorization_endpoint")?,
311 token_endpoint: field("token_endpoint")?,
312 jwks_uri: field("jwks_uri")?,
313 };
314 if provider.issuer.trim_end_matches('/') != self.config.issuer {
317 return Err(format!(
318 "`{url}` says its issuer is `{}`, which is not `{}`",
319 provider.issuer, self.config.issuer
320 ));
321 }
322 let host = url_host(&self.config.issuer, self.config.in_cluster)?;
327 for (name, endpoint) in [
328 ("authorization_endpoint", &provider.authorization_endpoint),
329 ("token_endpoint", &provider.token_endpoint),
330 ("jwks_uri", &provider.jwks_uri),
331 ] {
332 let elsewhere = url_host(endpoint, self.config.in_cluster)?;
333 if elsewhere != host {
334 return Err(format!(
335 "`{name}` is on `{elsewhere}`, and this relying party only reaches `{host}`"
336 ));
337 }
338 }
339 Ok(provider)
340 }
341
342 fn get(&self, url: &str) -> Result<String, String> {
343 let target = Target::parse(url, self.config.in_cluster)?;
344 let reply = self
345 .http
346 .fetch(
347 &Request {
348 host: Arc::from(target.host.as_str()),
349 port: target.port,
350 tls: target.tls,
351 method: Arc::from("GET"),
352 path: Arc::from(target.path.as_str()),
353 headers: vec![(Arc::from("accept"), Arc::from("application/json"))],
354 body: Arc::from(""),
355 },
356 &beck_core::net::Stop::never(),
357 )
358 .map_err(|e| format!("`{url}` was not reached: {e:?}"))?;
359 if reply.status != 200 {
360 return Err(format!("`{url}` answered {}", reply.status));
361 }
362 Ok(reply.body.to_string())
363 }
364
365 pub fn verify_id_token(&self, token: &str, nonce: Option<&str>) -> Result<Verified, Refusal> {
373 let (signed, signature, header, payload) = split(token)?;
374 let alg = header
375 .get("alg")
376 .and_then(|v| v.as_str())
377 .ok_or_else(|| Refusal::invalid("the token's header names no algorithm"))?;
378 let alg = Algorithm::named(alg).ok_or_else(|| {
379 Refusal::invalid(format!(
380 "`{alg}` is not an algorithm this relying party verifies"
381 ))
382 })?;
383 let kid = header.get("kid").and_then(|v| v.as_str());
384
385 let keys = self.keys.read().expect("not poisoned");
386 let candidates: Vec<&Key> = keys
387 .keys
388 .iter()
389 .filter(|k| k.usable_for(alg, kid))
390 .collect();
391 if candidates.is_empty() {
392 self.stale.store(true, Ordering::Relaxed);
395 return Err(Refusal::invalid(match kid {
396 Some(kid) => format!("no key `{kid}` for {alg:?} in the issuer's key set"),
397 None => format!("no {alg:?} key in the issuer's key set"),
398 }));
399 }
400 if !candidates
401 .iter()
402 .any(|k| k.verifies(alg, signed.as_bytes(), &signature))
403 {
404 return Err(Refusal::invalid("the signature does not verify"));
405 }
406 drop(keys);
407
408 self.check_claims(&payload, nonce)
409 }
410
411 fn check_claims(
412 &self,
413 payload: &serde_json::Value,
414 nonce: Option<&str>,
415 ) -> Result<Verified, Refusal> {
416 let str_claim = |name: &str| payload.get(name).and_then(|v| v.as_str());
417
418 match str_claim("iss") {
419 Some(iss) if iss.trim_end_matches('/') == self.config.issuer => {}
420 Some(iss) => {
421 return Err(Refusal::invalid(format!(
422 "the token is from `{iss}`, not `{}`",
423 self.config.issuer
424 )))
425 }
426 None => return Err(Refusal::invalid("the token names no issuer")),
427 }
428
429 let audiences = audiences(payload);
433 if !audiences.iter().any(|a| a == &self.config.client_id) {
434 return Err(Refusal::invalid(format!(
435 "the token's audience is {audiences:?}, which does not include `{}`",
436 self.config.client_id
437 )));
438 }
439 if audiences.len() > 1 {
440 match str_claim("azp") {
441 Some(azp) if azp == self.config.client_id => {}
442 _ => return Err(Refusal::invalid(
443 "the token has several audiences and its authorized party is not this client",
444 )),
445 }
446 }
447
448 let now = self.clock.now_millis();
449 let seconds = |name: &str| payload.get(name).and_then(|v| v.as_i64());
450 let exp = seconds("exp").ok_or_else(|| Refusal::invalid("the token has no expiry"))?;
451 let expires_at = exp.saturating_mul(1_000);
452 if now.saturating_sub(CLOCK_SKEW_MS) >= expires_at {
453 return Err(Refusal::expired(format!(
454 "the token expired at {expires_at} and it is {now}"
455 )));
456 }
457 if let Some(nbf) = seconds("nbf") {
458 if now.saturating_add(CLOCK_SKEW_MS) < nbf.saturating_mul(1_000) {
459 return Err(Refusal::invalid("the token is not valid yet"));
460 }
461 }
462
463 if let Some(expected) = nonce {
464 match str_claim("nonce") {
465 Some(got) if got == expected => {}
466 Some(_) => return Err(Refusal::invalid("the token replies to another login")),
467 None => return Err(Refusal::invalid("the token carries no nonce")),
468 }
469 }
470
471 let subject = str_claim(&self.config.actor_claim)
472 .filter(|s| !s.is_empty())
473 .ok_or_else(|| {
474 Refusal::invalid(format!(
475 "the token has no `{}` to name an actor with",
476 self.config.actor_claim
477 ))
478 })?
479 .to_string();
480
481 Ok(Verified {
482 subject,
483 claims: person_claims(payload),
484 expires_at_millis: expires_at,
485 })
486 }
487
488 pub fn begin_login(&self, return_to: &str) -> Result<Login, String> {
495 let provider = self
496 .provider()
497 .ok_or_else(|| "the identity provider has not been discovered yet".to_string())?;
498 let return_to = if return_to.starts_with('/') && !return_to.starts_with("//") {
499 return_to
500 } else {
501 "/"
502 };
503 let state = random_token();
504 let nonce = random_token();
505 let verifier = random_token();
506 let challenge = beck_core::digest::base64_encode_bytes(
507 aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, verifier.as_bytes()).as_ref(),
508 );
509
510 let scopes = if self.config.scopes.split_whitespace().any(|s| s == "openid") {
511 self.config.scopes.clone()
512 } else {
513 format!("openid {}", self.config.scopes)
514 };
515 let query = [
516 ("response_type", "code"),
517 ("client_id", &self.config.client_id),
518 ("redirect_uri", &self.config.redirect_uri),
519 ("scope", &scopes),
520 ("state", &state),
521 ("nonce", &nonce),
522 ("code_challenge", &challenge),
523 ("code_challenge_method", "S256"),
524 ]
525 .iter()
526 .map(|(k, v)| format!("{k}={}", percent_encode(v)))
527 .collect::<Vec<_>>()
528 .join("&");
529 let joiner = if provider.authorization_endpoint.contains('?') {
530 '&'
531 } else {
532 '?'
533 };
534
535 Ok(Login {
536 url: format!("{}{joiner}{query}", provider.authorization_endpoint),
537 transaction: self.seal_transaction(&Transaction {
538 state,
539 nonce,
540 verifier,
541 return_to: return_to.to_string(),
542 expires_at_millis: self.clock.now_millis() + LOGIN_WINDOW_MS,
543 }),
544 })
545 }
546
547 pub fn complete_login(&self, query: &str, transaction: &str) -> Result<Completion, String> {
552 let tx = self.open_transaction(transaction)?;
553 let params = query_params(query);
554 let got = |name: &str| {
555 params
556 .iter()
557 .find(|(k, _)| k == name)
558 .map(|(_, v)| v.clone())
559 };
560
561 if let Some(error) = got("error") {
562 return Err(format!(
563 "the identity provider refused the login: {error}{}",
564 got("error_description")
565 .map(|d| format!(" ({d})"))
566 .unwrap_or_default()
567 ));
568 }
569 let state = got("state").ok_or_else(|| "the reply carries no state".to_string())?;
570 if !beck_core::digest::same(&state, &tx.state) {
573 return Err("the reply's state is not the one this login asked for".to_string());
574 }
575 let code = got("code").ok_or_else(|| "the reply carries no code".to_string())?;
576
577 let id_token = self.exchange(&code, &tx.verifier)?;
578 let verified = self
579 .verify_id_token(&id_token, Some(&tx.nonce))
580 .map_err(|r| r.why)?;
581 Ok(Completion {
582 id_token,
583 verified,
584 return_to: tx.return_to,
585 })
586 }
587
588 fn exchange(&self, code: &str, verifier: &str) -> Result<String, String> {
589 let provider = self
590 .provider()
591 .ok_or_else(|| "the identity provider has not been discovered yet".to_string())?;
592 let target = Target::parse(&provider.token_endpoint, self.config.in_cluster)?;
593
594 let mut form = vec![
595 ("grant_type", "authorization_code".to_string()),
596 ("code", code.to_string()),
597 ("redirect_uri", self.config.redirect_uri.clone()),
598 ("client_id", self.config.client_id.clone()),
599 ("code_verifier", verifier.to_string()),
600 ];
601 let mut headers = vec![
602 (
603 Arc::from("content-type"),
604 Arc::from("application/x-www-form-urlencoded"),
605 ),
606 (Arc::from("accept"), Arc::from("application/json")),
607 ];
608 if let Some(secret) = &self.config.client_secret {
611 let basic = beck_core::digest::base64_encode(&format!(
612 "{}:{secret}",
613 percent_encode(&self.config.client_id)
614 ));
615 headers.push((
616 Arc::from("authorization"),
617 Arc::from(format!("Basic {basic}")),
618 ));
619 } else {
620 form.retain(|(k, _)| *k != "client_secret");
621 }
622 let body = form
623 .iter()
624 .map(|(k, v)| format!("{k}={}", percent_encode(v)))
625 .collect::<Vec<_>>()
626 .join("&");
627 headers.push((
628 Arc::from("content-length"),
629 Arc::from(body.len().to_string()),
630 ));
631
632 let reply = self
633 .http
634 .fetch(
635 &Request {
636 host: Arc::from(target.host.as_str()),
637 port: target.port,
638 tls: target.tls,
639 method: Arc::from("POST"),
640 path: Arc::from(target.path.as_str()),
641 headers,
642 body: Arc::from(body.as_str()),
643 },
644 &beck_core::net::Stop::never(),
645 )
646 .map_err(|e| format!("the token endpoint was not reached: {e:?}"))?;
647 if reply.status != 200 {
648 return Err(format!("the token endpoint answered {}", reply.status));
651 }
652 let doc: serde_json::Value = serde_json::from_str(&reply.body)
653 .map_err(|e| format!("the token endpoint did not answer JSON: {e}"))?;
654 doc.get("id_token")
655 .and_then(|v| v.as_str())
656 .map(|s| s.to_string())
657 .ok_or_else(|| "the token endpoint's answer carries no id_token".to_string())
658 }
659
660 fn seal_transaction(&self, tx: &Transaction) -> String {
663 let payload = format!(
664 "{}.{}.{}.{}.{}",
665 tx.state,
666 tx.nonce,
667 tx.verifier,
668 tx.expires_at_millis,
669 beck_core::digest::base64_encode(&tx.return_to)
670 );
671 let mac = blake3::keyed_hash(&self.seal, payload.as_bytes());
672 format!("{payload}.{}", mac.to_hex())
673 }
674
675 fn open_transaction(&self, sealed: &str) -> Result<Transaction, String> {
676 let (payload, mac) = sealed
677 .rsplit_once('.')
678 .ok_or_else(|| "the login has no cookie to check against".to_string())?;
679 let expected = blake3::keyed_hash(&self.seal, payload.as_bytes());
680 let given: blake3::Hash = mac
681 .parse()
682 .map_err(|_| "the login cookie is malformed".to_string())?;
683 if expected != given {
684 return Err("the login cookie was not sealed by this process".to_string());
685 }
686 let parts: Vec<&str> = payload.split('.').collect();
687 let [state, nonce, verifier, expiry, return_to] = parts[..] else {
688 return Err("the login cookie is malformed".to_string());
689 };
690 let expires_at_millis: i64 = expiry
691 .parse()
692 .map_err(|_| "the login cookie is malformed".to_string())?;
693 if self.clock.now_millis() >= expires_at_millis {
694 return Err("the login took longer than the window allows".to_string());
695 }
696 Ok(Transaction {
697 state: state.to_string(),
698 nonce: nonce.to_string(),
699 verifier: verifier.to_string(),
700 return_to: beck_core::digest::base64_decode(return_to)
701 .map_err(|_| "the login cookie is malformed".to_string())?,
702 expires_at_millis,
703 })
704 }
705}
706
707#[derive(Clone, Debug, PartialEq, Eq)]
709pub struct Verified {
710 pub subject: String,
711 pub claims: BTreeMap<Arc<str>, Arc<str>>,
712 pub expires_at_millis: i64,
713}
714
715#[derive(Clone, Debug)]
717pub struct Login {
718 pub url: String,
719 pub transaction: String,
723}
724
725#[derive(Clone, Debug)]
727pub struct Completion {
728 pub id_token: String,
730 pub verified: Verified,
731 pub return_to: String,
732}
733
734#[derive(Clone, Debug)]
735struct Transaction {
736 state: String,
737 nonce: String,
738 verifier: String,
739 return_to: String,
740 expires_at_millis: i64,
741}
742
743impl Identity for RelyingParty {
744 fn verify(&self, claim: &str) -> Result<Actor, Rejected> {
750 if claim.is_empty() {
751 return Err(Rejected::Missing);
752 }
753 match self.verify_id_token(claim, None) {
754 Ok(v) => Ok(Actor::verified(&v.subject, v.claims)),
755 Err(refusal) => {
756 tracing::warn!(why = %refusal.why, "an id token did not verify");
759 Err(refusal.client)
760 }
761 }
762 }
763
764 fn kind(&self) -> &'static str {
765 "oidc"
766 }
767
768 fn login(&self) -> Option<&RelyingParty> {
769 Some(self)
770 }
771}
772
773#[derive(Clone, Copy, Debug, PartialEq, Eq)]
785enum Algorithm {
786 Rs256,
787 Rs384,
788 Rs512,
789 Ps256,
790 Ps384,
791 Ps512,
792 Es256,
793 Es384,
794}
795
796impl Algorithm {
797 fn named(alg: &str) -> Option<Algorithm> {
798 Some(match alg {
799 "RS256" => Algorithm::Rs256,
800 "RS384" => Algorithm::Rs384,
801 "RS512" => Algorithm::Rs512,
802 "PS256" => Algorithm::Ps256,
803 "PS384" => Algorithm::Ps384,
804 "PS512" => Algorithm::Ps512,
805 "ES256" => Algorithm::Es256,
806 "ES384" => Algorithm::Es384,
807 _ => return None,
808 })
809 }
810
811 fn family(self) -> Family {
812 match self {
813 Algorithm::Es256 => Family::Ec("P-256"),
814 Algorithm::Es384 => Family::Ec("P-384"),
815 _ => Family::Rsa,
816 }
817 }
818
819 fn rsa(self) -> &'static RsaParameters {
820 match self {
821 Algorithm::Rs256 => &signature::RSA_PKCS1_2048_8192_SHA256,
822 Algorithm::Rs384 => &signature::RSA_PKCS1_2048_8192_SHA384,
823 Algorithm::Rs512 => &signature::RSA_PKCS1_2048_8192_SHA512,
824 Algorithm::Ps256 => &signature::RSA_PSS_2048_8192_SHA256,
825 Algorithm::Ps384 => &signature::RSA_PSS_2048_8192_SHA384,
826 Algorithm::Ps512 => &signature::RSA_PSS_2048_8192_SHA512,
827 Algorithm::Es256 | Algorithm::Es384 => &signature::RSA_PKCS1_2048_8192_SHA256,
831 }
832 }
833
834 fn ecdsa(self) -> &'static EcdsaVerificationAlgorithm {
838 match self {
839 Algorithm::Es384 => &signature::ECDSA_P384_SHA384_FIXED,
840 _ => &signature::ECDSA_P256_SHA256_FIXED,
841 }
842 }
843}
844
845#[derive(Clone, Copy, Debug, PartialEq, Eq)]
846enum Family {
847 Rsa,
848 Ec(&'static str),
849}
850
851#[derive(Clone, Debug, PartialEq, Eq)]
853struct Key {
854 kid: Option<String>,
855 alg: Option<String>,
859 material: Material,
860}
861
862#[derive(Clone, Debug, PartialEq, Eq)]
863enum Material {
864 Rsa {
865 n: Vec<u8>,
866 e: Vec<u8>,
867 },
868 Ec {
870 curve: &'static str,
871 point: Vec<u8>,
872 },
873}
874
875impl Key {
876 fn usable_for(&self, alg: Algorithm, kid: Option<&str>) -> bool {
877 if let Some(kid) = kid {
880 if self.kid.as_deref() != Some(kid) {
881 return false;
882 }
883 }
884 if let Some(declared) = &self.alg {
885 if Algorithm::named(declared) != Some(alg) {
886 return false;
887 }
888 }
889 match (&self.material, alg.family()) {
890 (Material::Rsa { .. }, Family::Rsa) => true,
891 (Material::Ec { curve, .. }, Family::Ec(wanted)) => *curve == wanted,
892 _ => false,
893 }
894 }
895
896 fn verifies(&self, alg: Algorithm, message: &[u8], signature: &[u8]) -> bool {
897 match &self.material {
898 Material::Rsa { n, e } => RsaPublicKeyComponents { n, e }
899 .verify(alg.rsa(), message, signature)
900 .is_ok(),
901 Material::Ec { point, .. } => UnparsedPublicKey::new(alg.ecdsa(), point)
902 .verify(message, signature)
903 .is_ok(),
904 }
905 }
906}
907
908fn split(token: &str) -> Result<(&str, Vec<u8>, serde_json::Value, serde_json::Value), Refusal> {
914 let mut parts = token.split('.');
915 let (Some(header), Some(payload), Some(signature), None) =
916 (parts.next(), parts.next(), parts.next(), parts.next())
917 else {
918 return Err(Refusal::invalid(
919 "a JWS compact serialization has exactly three segments",
920 ));
921 };
922 let signed = &token[..header.len() + 1 + payload.len()];
923 let signature = beck_core::digest::base64_decode_bytes(signature)
924 .map_err(|e| Refusal::invalid(format!("the signature segment is not base64url: {e}")))?;
925 let decode = |segment: &str, what: &str| -> Result<serde_json::Value, Refusal> {
926 let bytes = beck_core::digest::base64_decode_bytes(segment)
927 .map_err(|e| Refusal::invalid(format!("the {what} is not base64url: {e}")))?;
928 serde_json::from_slice(&bytes)
929 .map_err(|e| Refusal::invalid(format!("the {what} is not JSON: {e}")))
930 };
931 Ok((
932 signed,
933 signature,
934 decode(header, "header")?,
935 decode(payload, "payload")?,
936 ))
937}
938
939fn parse_jwks(body: &str) -> Result<Vec<Key>, String> {
940 let doc: serde_json::Value =
941 serde_json::from_str(body).map_err(|e| format!("the key set is not JSON: {e}"))?;
942 let keys = doc
943 .get("keys")
944 .and_then(|k| k.as_array())
945 .ok_or_else(|| "the key set has no `keys` array".to_string())?;
946 Ok(keys.iter().filter_map(parse_jwk).collect())
950}
951
952fn parse_jwk(jwk: &serde_json::Value) -> Option<Key> {
953 let field = |name: &str| jwk.get(name).and_then(|v| v.as_str());
954 if matches!(field("use"), Some(u) if u != "sig") {
956 return None;
957 }
958 let bytes = |name: &str| beck_core::digest::base64_decode_bytes(field(name)?).ok();
959 let material = match field("kty")? {
960 "RSA" => Material::Rsa {
961 n: bytes("n")?,
962 e: bytes("e")?,
963 },
964 "EC" => {
965 let curve = match field("crv")? {
966 "P-256" => "P-256",
967 "P-384" => "P-384",
968 _ => return None,
969 };
970 let (x, y) = (bytes("x")?, bytes("y")?);
971 let width = if curve == "P-256" { 32 } else { 48 };
974 if x.len() != width || y.len() != width {
975 return None;
976 }
977 let mut point = Vec::with_capacity(1 + 2 * width);
978 point.push(0x04);
979 point.extend_from_slice(&x);
980 point.extend_from_slice(&y);
981 Material::Ec { curve, point }
982 }
983 _ => return None,
984 };
985 Some(Key {
986 kid: field("kid").map(|s| s.to_string()),
987 alg: field("alg").map(|s| s.to_string()),
988 material,
989 })
990}
991
992fn audiences(payload: &serde_json::Value) -> Vec<String> {
993 match payload.get("aud") {
994 Some(serde_json::Value::String(s)) => vec![s.clone()],
995 Some(serde_json::Value::Array(xs)) => xs
996 .iter()
997 .filter_map(|x| x.as_str().map(|s| s.to_string()))
998 .collect(),
999 _ => Vec::new(),
1000 }
1001}
1002
1003fn person_claims(payload: &serde_json::Value) -> BTreeMap<Arc<str>, Arc<str>> {
1009 let mut out = BTreeMap::new();
1010 let Some(fields) = payload.as_object() else {
1011 return out;
1012 };
1013 for (name, value) in fields {
1014 if PROTOCOL_CLAIMS.contains(&name.as_str()) {
1015 continue;
1016 }
1017 let rendered = match value {
1018 serde_json::Value::String(s) => s.clone(),
1019 serde_json::Value::Bool(b) => b.to_string(),
1020 serde_json::Value::Number(n) => n.to_string(),
1021 serde_json::Value::Array(xs) => {
1022 let parts: Vec<&str> = xs.iter().filter_map(|x| x.as_str()).collect();
1023 if parts.len() != xs.len() {
1024 continue;
1025 }
1026 parts.join(" ")
1027 }
1028 _ => continue,
1029 };
1030 out.insert(Arc::from(name.as_str()), Arc::from(rendered.as_str()));
1031 }
1032 out
1033}
1034
1035#[derive(Clone, Debug, PartialEq, Eq)]
1041struct Target {
1042 host: String,
1043 port: u16,
1044 path: String,
1045 tls: bool,
1047}
1048
1049impl Target {
1050 fn parse(url: &str, in_cluster: bool) -> Result<Target, String> {
1057 let (rest, tls) = match url.strip_prefix("https://") {
1058 Some(rest) => (rest, true),
1059 None => match url.strip_prefix("http://").filter(|_| in_cluster) {
1060 Some(rest) => (rest, false),
1061 None => {
1062 return Err(format!(
1063 "`{url}` is not an https URL, and this one has to be"
1064 ))
1065 }
1066 },
1067 };
1068 let (authority, path) = match rest.find('/') {
1069 Some(at) => (&rest[..at], &rest[at..]),
1070 None => (rest, "/"),
1071 };
1072 if authority.contains('@') {
1073 return Err(format!("`{url}` carries credentials in its authority"));
1074 }
1075 let (host, port) = match authority.rsplit_once(':') {
1076 Some((h, p)) => (
1077 h,
1078 p.parse::<u16>()
1079 .map_err(|_| format!("`{url}` has no readable port"))?,
1080 ),
1081 None => (authority, if tls { 443 } else { 80 }),
1082 };
1083 if !beck_core::net::is_nameable_host(host) {
1084 return Err(format!(
1085 "`{host}` is not a host an egress rule could be written for"
1086 ));
1087 }
1088 Ok(Target {
1089 host: host.to_string(),
1090 port,
1091 path: path.to_string(),
1092 tls,
1093 })
1094 }
1095}
1096
1097fn url_host(url: &str, in_cluster: bool) -> Result<String, String> {
1098 Ok(Target::parse(url, in_cluster)?.host)
1099}
1100
1101fn percent_encode(text: &str) -> String {
1103 let mut out = String::with_capacity(text.len());
1104 for byte in text.as_bytes() {
1105 match byte {
1106 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
1107 out.push(char::from(*byte))
1108 }
1109 _ => out.push_str(&format!("%{byte:02X}")),
1110 }
1111 }
1112 out
1113}
1114
1115fn percent_decode(text: &str) -> String {
1116 let bytes = text.as_bytes();
1117 let mut out = Vec::with_capacity(bytes.len());
1118 let mut i = 0;
1119 while i < bytes.len() {
1120 match bytes[i] {
1121 b'%' if i + 2 < bytes.len() => {
1122 match u8::from_str_radix(&text[i + 1..i + 3], 16) {
1123 Ok(b) => {
1124 out.push(b);
1125 i += 3;
1126 }
1127 Err(_) => {
1128 out.push(bytes[i]);
1129 i += 1;
1130 }
1131 }
1132 continue;
1133 }
1134 b'+' => out.push(b' '),
1135 b => out.push(b),
1136 }
1137 i += 1;
1138 }
1139 String::from_utf8_lossy(&out).into_owned()
1140}
1141
1142pub fn query_params(query: &str) -> Vec<(String, String)> {
1149 query
1150 .split('&')
1151 .filter(|pair| !pair.is_empty())
1152 .map(|pair| match pair.split_once('=') {
1153 Some((k, v)) => (percent_decode(k), percent_decode(v)),
1154 None => (percent_decode(pair), String::new()),
1155 })
1156 .collect()
1157}
1158
1159fn random_token() -> String {
1162 let mut bytes = [0u8; 32];
1163 aws_lc_rs::rand::fill(&mut bytes).expect("the system random source answers");
1164 beck_core::digest::base64_encode_bytes(&bytes)
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170
1171 #[test]
1172 fn only_an_https_url_with_a_nameable_host_is_a_target() {
1173 assert_eq!(
1174 Target::parse("https://login.acme.com/authorize", false),
1175 Ok(Target {
1176 host: "login.acme.com".into(),
1177 port: 443,
1178 path: "/authorize".into(),
1179 tls: true,
1180 })
1181 );
1182 assert_eq!(
1183 Target::parse("https://login.acme.com:8443", false),
1184 Ok(Target {
1185 host: "login.acme.com".into(),
1186 port: 8443,
1187 path: "/".into(),
1188 tls: true,
1189 })
1190 );
1191 assert!(Target::parse("http://login.acme.com/", false).is_err());
1193 assert!(Target::parse("https://user:[email protected]/", false).is_err());
1194 assert!(Target::parse("https://not a host/", false).is_err());
1195 }
1196
1197 #[test]
1200 fn a_plaintext_issuer_is_a_target_only_for_a_provisioned_provider() {
1201 assert_eq!(
1202 Target::parse("http://todo-identity:8080/realms/todo", true),
1203 Ok(Target {
1204 host: "todo-identity".into(),
1205 port: 8080,
1206 path: "/realms/todo".into(),
1207 tls: false,
1208 })
1209 );
1210 assert!(Target::parse("http://todo-identity:8080/realms/todo", false).is_err());
1212 assert!(Target::parse("http://user:pw@todo-identity/", true).is_err());
1215 assert!(Target::parse("http://not a host/", true).is_err());
1216 }
1217
1218 #[test]
1219 fn a_query_is_read_the_way_a_browser_wrote_it() {
1220 assert_eq!(
1221 query_params("code=a%2Fb&state=x+y"),
1222 vec![
1223 ("code".to_string(), "a/b".to_string()),
1224 ("state".to_string(), "x y".to_string())
1225 ]
1226 );
1227 assert_eq!(query_params(""), Vec::new());
1228 }
1229
1230 #[test]
1231 fn the_algorithms_that_are_not_here_are_the_point() {
1232 assert_eq!(Algorithm::named("RS256"), Some(Algorithm::Rs256));
1233 assert_eq!(Algorithm::named("ES256"), Some(Algorithm::Es256));
1234 assert_eq!(Algorithm::named("none"), None);
1236 assert_eq!(Algorithm::named("HS256"), None);
1237 assert_eq!(Algorithm::named("EdDSA"), None);
1238 }
1239
1240 #[test]
1241 fn a_key_set_keeps_what_it_understands_and_drops_the_rest() {
1242 let set = parse_jwks(
1243 r#"{"keys":[
1244 {"kty":"RSA","kid":"a","n":"AQAB","e":"AQAB"},
1245 {"kty":"RSA","kid":"enc","use":"enc","n":"AQAB","e":"AQAB"},
1246 {"kty":"OKP","kid":"ed","crv":"Ed25519","x":"AQAB"}
1247 ]}"#,
1248 )
1249 .expect("a key set");
1250 assert_eq!(set.len(), 1, "{set:?}");
1251 assert_eq!(set[0].kid.as_deref(), Some("a"));
1252 }
1253
1254 #[test]
1255 fn a_claim_map_carries_the_person_and_not_the_envelope() {
1256 let payload = serde_json::json!({
1257 "sub": "ana", "email": "[email protected]", "email_verified": true,
1258 "groups": ["admin", "billing"], "address": {"country": "GB"},
1259 "iss": "https://login.acme.com", "exp": 1, "nonce": "n"
1260 });
1261 let claims = person_claims(&payload);
1262 assert_eq!(claims.get("sub").map(|s| s.as_ref()), Some("ana"));
1263 assert_eq!(
1264 claims.get("email_verified").map(|s| s.as_ref()),
1265 Some("true")
1266 );
1267 assert_eq!(
1268 claims.get("groups").map(|s| s.as_ref()),
1269 Some("admin billing")
1270 );
1271 assert!(!claims.contains_key("iss"), "{claims:?}");
1272 assert!(!claims.contains_key("exp"), "{claims:?}");
1273 assert!(!claims.contains_key("nonce"), "{claims:?}");
1274 assert!(!claims.contains_key("address"), "{claims:?}");
1277 }
1278}