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}