jwks_cache/http/
semantics.rs1use http::{Method, Request, Response, Uri};
5use http_cache_semantics::{AfterResponse, CachePolicy};
6use crate::{_prelude::*, http::client::HttpExchange, registry::IdentityProviderRegistration};
8
9#[derive(Clone, Debug)]
11pub struct Freshness {
12 pub ttl: Duration,
15 pub policy: CachePolicy,
17}
18
19#[derive(Debug)]
21pub struct Revalidation {
22 pub freshness: Freshness,
24 pub response: Response<()>,
26 pub modified: bool,
28}
29
30pub fn base_request(registration: &IdentityProviderRegistration) -> Result<Request<()>> {
32 let uri = parse_uri(registration)?;
33
34 Request::builder()
35 .method(Method::GET)
36 .uri(uri)
37 .header("accept", "application/json")
38 .body(())
39 .map_err(Error::from)
40}
41
42pub fn evaluate_freshness(
44 registration: &IdentityProviderRegistration,
45 exchange: &HttpExchange,
46) -> Result<Freshness> {
47 let policy = CachePolicy::new(&exchange.request, &exchange.response);
48 let storable = policy.is_storable();
49 let ttl = if storable {
50 clamp_ttl(
51 policy.time_to_live(SystemTime::now()),
52 registration.min_ttl,
53 registration.max_ttl,
54 )
55 } else {
56 registration.min_ttl
57 };
58
59 tracing::debug!(ttl=?ttl, storable, "evaluated freshness");
60
61 Ok(Freshness { ttl, policy })
62}
63
64pub fn evaluate_revalidation(
66 registration: &IdentityProviderRegistration,
67 policy: &CachePolicy,
68 request: &Request<()>,
69 response: &Response<()>,
70) -> Result<Revalidation> {
71 let now = SystemTime::now();
72 let outcome = policy.after_response(request, response, now);
73 let (policy, parts, modified) = match outcome {
74 AfterResponse::NotModified(policy, parts) => (policy, parts, false),
75 AfterResponse::Modified(policy, parts) => (policy, parts, true),
76 };
77 let response = Response::from_parts(parts, ());
78 let ttl = clamp_ttl(policy.time_to_live(now), registration.min_ttl, registration.max_ttl);
79
80 Ok(Revalidation { freshness: Freshness { ttl, policy }, response, modified })
81}
82
83fn parse_uri(registration: &IdentityProviderRegistration) -> Result<Uri> {
84 registration.jwks_url.as_str().parse::<Uri>().map_err(|err| Error::Validation {
85 field: "jwks_url",
86 reason: format!("Failed to convert URL to http::Uri: {err}."),
87 })
88}
89
90fn clamp_ttl(ttl: Duration, min: Duration, max: Duration) -> Duration {
91 if ttl < min {
92 min
93 } else if ttl > max {
94 max
95 } else {
96 ttl
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use http::{
104 StatusCode,
105 header::{CACHE_CONTROL, ETAG},
106 };
107 use http_cache_semantics::BeforeRequest;
108 use super::*;
110
111 fn make_registration() -> IdentityProviderRegistration {
112 IdentityProviderRegistration::new(
113 "tenant",
114 "provider",
115 "https://example.com/.well-known/jwks.json",
116 )
117 .expect("registration")
118 }
119
120 #[test]
121 fn clamps_ttl_to_registration_bounds() {
122 let mut registration = make_registration();
123
124 registration.min_ttl = Duration::from_secs(30);
125 registration.max_ttl = Duration::from_secs(60);
126
127 let request = base_request(®istration).expect("request");
128 let response = Response::builder()
129 .status(StatusCode::OK)
130 .header(CACHE_CONTROL, "max-age=5")
131 .body(())
132 .expect("response");
133 let exchange = HttpExchange::new(request, response, Duration::from_millis(12));
134 let freshness = evaluate_freshness(®istration, &exchange).expect("freshness");
135
136 assert_eq!(freshness.ttl, Duration::from_secs(30));
137 }
138
139 #[test]
140 fn adds_etag_to_conditional_revalidation_headers() {
141 let mut registration = make_registration();
142
143 registration.require_https = false;
144 registration.min_ttl = Duration::from_secs(1);
145 registration.max_ttl = Duration::from_secs(10);
146
147 let request = base_request(®istration).expect("request");
148 let response = Response::builder()
149 .status(StatusCode::OK)
150 .header(CACHE_CONTROL, "max-age=1")
151 .header(ETAG, "\"jwks-tag\"")
152 .body(())
153 .expect("response");
154 let exchange = HttpExchange::new(request.clone(), response, Duration::from_millis(8));
155 let freshness = evaluate_freshness(®istration, &exchange).expect("freshness");
156 let request = base_request(®istration).expect("request");
157 let decision =
158 freshness.policy.before_request(&request, SystemTime::now() + Duration::from_secs(5));
159
160 match decision {
161 BeforeRequest::Stale { request, .. } => {
162 let if_none_match = request.headers.get("if-none-match");
163
164 assert_eq!(
165 if_none_match.and_then(|value| value.to_str().ok()),
166 Some("\"jwks-tag\"")
167 );
168 },
169 BeforeRequest::Fresh(_) => {
170 panic!("expected stale decision triggering conditional headers")
171 },
172 }
173 }
174}