jsonwebtokens_cognito/lib.rs
1use std::sync::Arc;
2use std::sync::RwLock;
3use std::time::{Duration, Instant};
4use std::collections::HashMap;
5
6use serde::{Deserialize};
7use serde_json::value::Value;
8use serde_json;
9
10use reqwest::{self, Response};
11
12use jsonwebtokens as jwt;
13use jwt::{Algorithm, AlgorithmID, Verifier, VerifierBuilder};
14
15mod error;
16pub use error::{Error, ErrorDetails};
17
18#[derive(Debug, Deserialize, Clone)]
19struct RSAKey {
20 kid: String,
21 alg: String,
22 n: String,
23 e: String,
24}
25
26#[derive(Debug, Deserialize)]
27struct JwkSet {
28 keys: Vec<RSAKey>,
29}
30
31#[derive(Debug, Clone)]
32struct Cache {
33 last_jwks_get_time: Option<Instant>,
34 algorithms: HashMap<String, Arc<Algorithm>>,
35}
36
37/// Abstracts a remote Amazon Cognito JWKS key set
38///
39/// The key set represents the public key information for one or more RSA keys that
40/// Amazon Cognito uses to sign tokens. To verify a token from Cognito the token's
41/// `kid` is used to look up the corresponding public key from this set which can
42/// be used to verify the token's signature.
43///
44/// Building on top of the [Verifier](https://docs.rs/jsonwebtokens/1.0.0-alpha.8/jsonwebtokens/struct.Verifier.html)
45/// API from [jsonwebtokens](https://crates.io/crates/jsonwebtokens), a KeySet provides some
46/// helpers for building a [Verifier](https://docs.rs/jsonwebtokens/1.0.0-alpha.8/jsonwebtokens/struct.Verifier.html)
47/// for Cognito Access token claims or ID token claims - referencing the region and
48/// pool details used to construct the keyset.
49///
50/// Example:
51/// ```no_run
52/// # use jsonwebtokens_cognito::KeySet;
53/// # use async_std::prelude::*;
54/// # #[async_std::main]
55/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
56/// let keyset = KeySet::new("eu-west-1", "my-user-pool-id")?;
57/// let verifier = keyset.new_id_token_verifier(&["client-id-0", "client-id-1"])
58/// .string_equals("custom_claim0", "value")
59/// .string_equals("custom_claim1", "value")
60/// .build()?;
61/// # let token = "header.payload.signature";
62/// let claims = keyset.verify(token, &verifier).await?;
63/// # Ok(())
64/// # }
65/// ```
66///
67/// Internally a KeySet holds a cache of Algorithm structs (see the jsonwebtokens
68/// API for further details) where each Algorithm represents one RSA public key.
69///
70/// Although `keyset.verify()` can be very convenient, if you need to avoid network
71/// I/O when verifying tokens it's also possible to prefetch the remote JWKS key
72/// set ahead of time and `try_verify()` can be used to verify a token without any
73/// network I/O. This can be useful if you don't have an async context when
74/// verifying tokens.
75///
76/// ```no_run
77/// # use jsonwebtokens_cognito::KeySet;
78/// # use async_std::prelude::*;
79/// # #[async_std::main]
80/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
81/// let keyset = KeySet::new("eu-west-1", "my-user-pool-id")?;
82/// keyset.prefetch_jwks().await?;
83/// let verifier = keyset.new_id_token_verifier(&["client-id-0", "client-id-1"])
84/// .string_equals("custom_claim0", "value")
85/// .string_equals("custom_claim1", "value")
86/// .build()?;
87/// # let token = "header.payload.signature";
88/// let claims = keyset.try_verify(token, &verifier)?;
89/// # Ok(())
90/// # }
91/// ```
92///
93/// It's also possible to perform cache lookups directly to access an Algorithm if
94/// you need to use the jsonwebtokens API directly:
95/// ```no_run
96/// # use jsonwebtokens_cognito::KeySet;
97/// # use jsonwebtokens as jwt;
98/// # use serde_json::value::Value;
99/// # use async_std::prelude::*;
100/// # #[async_std::main]
101/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
102/// # let token = "header.payload.signature";
103/// let keyset = KeySet::new("eu-west-1", "my-user-pool-id")?;
104/// keyset.prefetch_jwks().await?;
105///
106/// let verifier = keyset.new_id_token_verifier(&["client-id-0", "client-id-1"])
107/// .string_equals("custom_claim0", "value")
108/// .string_equals("custom_claim1", "value")
109/// .build()?;
110///
111/// let header = jwt::raw::decode_header_only(token)?;
112/// if let Some(Value::String(kid)) = header.get("kid") {
113/// let alg = keyset.try_cache_lookup_algorithm(kid)?;
114/// let claims = verifier.verify(token, &alg)?;
115///
116/// // Whoop!
117/// } else {
118/// Err(jwt::error::Error::MalformedToken(jwt::error::ErrorDetails::new("Missing kid")))?;
119/// };
120/// # Ok(())
121/// # }
122/// ```
123
124///
125#[derive(Debug, Clone)]
126pub struct KeySet {
127 _region: String,
128 _pool_id: String,
129 jwks_url: String,
130 iss: String,
131 cache: Arc<RwLock<Cache>>,
132 min_jwks_fetch_interval: Duration,
133}
134
135impl KeySet {
136
137 /// Constructs a key set that corresponds to a remote Json Web Key Set published
138 /// by Amazon for a given region and Cognito User Pool ID.
139 pub fn new(region: impl Into<String>,
140 pool_id: impl Into<String>
141 ) -> Result<Self, Error> {
142
143 let region_str = region.into();
144 let pool_id_str = pool_id.into();
145 let jwks_url = format!("https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json",
146 region_str, pool_id_str).into();
147 let iss = format!("https://cognito-idp.{}.amazonaws.com/{}", region_str, pool_id_str);
148
149 Ok(KeySet {
150 _region: region_str,
151 _pool_id: pool_id_str,
152 jwks_url: jwks_url,
153 iss: iss,
154 cache: Arc::new(RwLock::new(Cache {
155 last_jwks_get_time: None,
156 algorithms: HashMap::new()
157 })),
158 min_jwks_fetch_interval: Duration::from_secs(60),
159 })
160 }
161
162 /// Returns a `VerifierBuilder` that has been pre-configured to validate an
163 /// AWS Cognito ID token. This can be further configured for verifying other
164 /// custom claims before calling `.build()` to create a `Verifier`
165 pub fn new_id_token_verifier(&self, client_ids: &[&str]) -> VerifierBuilder {
166 let mut builder = Verifier::create();
167
168 builder
169 .string_equals("iss", &self.iss)
170 .string_equals_one_of("aud", client_ids)
171 .string_equals("token_use", "id");
172
173 builder
174 }
175
176 /// Set's the minimum time between attempts to fetch the remote JWKS key set
177 ///
178 /// By default this is one minute, to throttle requests in case there is a
179 /// transient network problem
180 pub fn set_min_jwks_fetch_interval(&mut self, interval: Duration) {
181 self.min_jwks_fetch_interval = interval;
182 }
183
184 /// Get's the minimum time between attempts to fetch the remote JWKS key set
185 pub fn min_jwks_fetch_interval(&mut self) -> Duration {
186 self.min_jwks_fetch_interval
187 }
188
189 /// Returns a `VerifierBuilder` that has been pre-configured to validate an
190 /// AWS Cognito access token. This can be further configured for verifying other
191 /// custom claims before calling `.build()` to create a `Verifier`
192 pub fn new_access_token_verifier(&self, client_ids: &[&str]) -> VerifierBuilder {
193 let mut builder = Verifier::create();
194
195 builder
196 .string_equals("iss", &self.iss)
197 .string_equals_one_of("client_id", client_ids)
198 .string_equals("token_use", "access");
199
200 builder
201 }
202
203 /// Looks for a cached Algorithm based on the given JWT token's key ID ('kid')
204 ///
205 /// This is a lower-level API in case you need to use the jsonwebtokens
206 /// Algorithm API directly.
207 ///
208 /// Returns an `Arc<Algorithm>` corresponding to the give key ID (`kid`) or returns
209 /// a `CacheMiss` error if the Algorithm / key is not cached.
210 pub fn try_cache_lookup_algorithm(&self, kid: &str) -> Result<Arc<Algorithm>, Error> {
211
212 // We unwrap, because poisoning would imply something else had gone
213 // badly wrong (there should be nothing that can cause a panic while
214 // holding the cache's lock)
215 let readable_cache = self.cache.read().unwrap();
216
217 let a = readable_cache.algorithms.get(kid);
218 if let Some(alg) = a {
219 return Ok(alg.clone());
220 } else {
221 return Err(Error::CacheMiss(readable_cache.last_jwks_get_time));
222 }
223 }
224
225 async fn wait_and_cache_lookup_algorithm(&self, kid: &str) -> Result<Arc<Algorithm>, Error> {
226 match self.try_cache_lookup_algorithm(kid) {
227 Err(Error::CacheMiss(last_update_time)) => {
228 let duration = match last_update_time {
229 Some(last_jwks_get_time) => Instant::now().duration_since(last_jwks_get_time),
230 None => self.min_jwks_fetch_interval
231 };
232
233 if duration < self.min_jwks_fetch_interval {
234 return Err(Error::NetworkError(ErrorDetails::new("Key set is currently unreachable (throttled)")))
235 }
236
237 self.prefetch_jwks().await?;
238 self.try_cache_lookup_algorithm(kid)
239 },
240 Err(e) => {
241 // try_cache_lookup_algorithm shouldn't return any other kind of error...
242 unreachable!("Unexpected error looking up JWT Algorithm for key ID: {:?}", e);
243 }
244 Ok(alg) => Ok(alg)
245 }
246 }
247
248 /// Verify a token's signature and its claims
249 pub async fn verify(
250 &self,
251 token: &str,
252 verifier: &Verifier
253 ) -> Result<serde_json::value::Value, Error> {
254
255 let header = jwt::raw::decode_header_only(token)?;
256
257 let kid = match header.get("kid") {
258 Some(Value::String(kid)) => kid,
259 _ => return Err(Error::NoKeyID()),
260 };
261
262 let algorithm = self.wait_and_cache_lookup_algorithm(kid).await?;
263
264 let claims = verifier.verify(token, &algorithm)?;
265 Ok(claims)
266 }
267
268 /// Verify a token's signature and its claims, given a specific unix epoch timestamp
269 pub async fn verify_for_time(
270 &self,
271 token: &str,
272 verifier: &Verifier,
273 time_now: u64
274 ) -> Result<jsonwebtokens::TokenData, Error> {
275
276 let header = jwt::raw::decode_header_only(token)?;
277
278 let kid = match header.get("kid") {
279 Some(Value::String(kid)) => kid,
280 _ => return Err(Error::NoKeyID()),
281 };
282
283 let algorithm = self.wait_and_cache_lookup_algorithm(kid).await?;
284
285 let token_data = verifier.verify_for_time(token, &algorithm, time_now)?;
286 Ok(token_data)
287 }
288
289 /// Try and verify a token's signature and claims without performing any network I/O
290 ///
291 /// To be able to verify a token in a synchronous context (but without blocking) this
292 /// API lets you try and verify a token, and if the required Algorithm / key has not
293 /// been cached yet then it will return a `CacheMiss` error.
294 pub fn try_verify(
295 &self,
296 token: &str,
297 verifier: &Verifier
298 ) -> Result<serde_json::value::Value, Error> {
299
300 let header = jwt::raw::decode_header_only(token)?;
301
302 let kid = match header.get("kid") {
303 Some(Value::String(kid)) => kid,
304 _ => return Err(Error::NoKeyID()),
305 };
306
307 let alg = self.try_cache_lookup_algorithm(kid)?;
308 let claims = verifier.verify(token, &alg)?;
309 Ok(claims)
310 }
311
312 /// Ensure the remote Json Web Key Set is downloaded and cached
313 pub async fn prefetch_jwks(&self) -> Result<(), Error> {
314 let resp: Response = reqwest::get(&self.jwks_url).await?;
315 let jwks: JwkSet = resp.json().await?;
316
317 // We unwrap, because poisoning would imply something else had gone
318 // badly wrong (there should be nothing that can cause a panic while
319 // holding the cache's lock)
320 let mut writeable_cache = self.cache.write().unwrap();
321
322 writeable_cache.last_jwks_get_time = Some(Instant::now());
323
324 for key in jwks.keys.into_iter() {
325 // For now we assume AWS Cognito only ever uses RS256 keys
326 if key.alg != "RS256" {
327 continue;
328 }
329 let mut algorithm = Algorithm::new_rsa_n_e_b64_verifier(AlgorithmID::RS256, &key.n, &key.e)?;
330 // By associating a kid here we will essentially be double checking
331 // that we only verify a token with the key matching its associated kid
332 // (once by us and jsonwebtokens will also check too)
333 algorithm.set_kid(&key.kid);
334 writeable_cache.algorithms.insert(key.kid.clone(), Arc::new(algorithm));
335 }
336
337 Ok(())
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 // TODO
344}