/rust/registry/src/index.crates.io-1949cf8c6b5b557f/jsonwebtoken-10.3.0/src/validation.rs
Line | Count | Source |
1 | | use std::borrow::Cow; |
2 | | use std::collections::HashSet; |
3 | | use std::fmt; |
4 | | use std::marker::PhantomData; |
5 | | |
6 | | use serde::de::{self, Visitor}; |
7 | | use serde::{Deserialize, Deserializer}; |
8 | | |
9 | | use crate::algorithms::Algorithm; |
10 | | use crate::errors::{ErrorKind, Result, new_error}; |
11 | | |
12 | | /// Contains the various validations that are applied after decoding a JWT. |
13 | | /// |
14 | | /// All time validation happen on UTC timestamps as seconds. |
15 | | /// |
16 | | /// ```rust |
17 | | /// use jsonwebtoken::{Validation, Algorithm}; |
18 | | /// |
19 | | /// let mut validation = Validation::new(Algorithm::HS256); |
20 | | /// validation.leeway = 5; |
21 | | /// // Setting audience |
22 | | /// validation.set_audience(&["Me"]); // a single string |
23 | | /// validation.set_audience(&["Me", "You"]); // array of strings |
24 | | /// // or issuer |
25 | | /// validation.set_issuer(&["Me"]); // a single string |
26 | | /// validation.set_issuer(&["Me", "You"]); // array of strings |
27 | | /// // Setting required claims |
28 | | /// validation.set_required_spec_claims(&["exp", "iss", "aud"]); |
29 | | /// ``` |
30 | | #[derive(Debug, Clone, PartialEq, Eq)] |
31 | | pub struct Validation { |
32 | | /// Which claims are required to be present before starting the validation. |
33 | | /// This does not interact with the various `validate_*`. If you remove `exp` from that list, you still need |
34 | | /// to set `validate_exp` to `false`. |
35 | | /// The only value that will be used are "exp", "nbf", "aud", "iss", "sub". Anything else will be ignored. |
36 | | /// |
37 | | /// Defaults to `{"exp"}` |
38 | | pub required_spec_claims: HashSet<String>, |
39 | | /// Add some leeway (in seconds) to the `exp` and `nbf` validation to |
40 | | /// account for clock skew. |
41 | | /// |
42 | | /// Defaults to `60`. |
43 | | pub leeway: u64, |
44 | | /// Reject a token some time (in seconds) before the `exp` to prevent |
45 | | /// expiration in transit over the network. |
46 | | /// |
47 | | /// The value is the inverse of `leeway`, subtracting from the validation time. |
48 | | /// |
49 | | /// Defaults to `0`. |
50 | | pub reject_tokens_expiring_in_less_than: u64, |
51 | | /// Whether to validate the `exp` field. |
52 | | /// |
53 | | /// It will return an error if the time in the `exp` field is past. |
54 | | /// |
55 | | /// Defaults to `true`. |
56 | | pub validate_exp: bool, |
57 | | /// Whether to validate the `nbf` field. |
58 | | /// |
59 | | /// It will return an error if the current timestamp is before the time in the `nbf` field. |
60 | | /// |
61 | | /// Validation only happens if `nbf` claim is present in the token. |
62 | | /// Adding `nbf` to `required_spec_claims` will make it required. |
63 | | /// |
64 | | /// Defaults to `false`. |
65 | | pub validate_nbf: bool, |
66 | | /// Whether to validate the `aud` field. |
67 | | /// |
68 | | /// It will return an error if the `aud` field is not a member of the audience provided. |
69 | | /// |
70 | | /// Validation only happens if `aud` claim is present in the token. |
71 | | /// Adding `aud` to `required_spec_claims` will make it required. |
72 | | /// |
73 | | /// Defaults to `true`. Very insecure to turn this off. Only do this if you know what you are doing. |
74 | | pub validate_aud: bool, |
75 | | /// Validation will check that the `aud` field is a member of the |
76 | | /// audience provided and will error otherwise. |
77 | | /// Use `set_audience` to set it |
78 | | /// |
79 | | /// Validation only happens if `aud` claim is present in the token. |
80 | | /// Adding `aud` to `required_spec_claims` will make it required. |
81 | | /// |
82 | | /// Defaults to `None`. |
83 | | pub aud: Option<HashSet<String>>, |
84 | | /// If it contains a value, the validation will check that the `iss` field is a member of the |
85 | | /// iss provided and will error otherwise. |
86 | | /// Use `set_issuer` to set it |
87 | | /// |
88 | | /// Validation only happens if `iss` claim is present in the token. |
89 | | /// Adding `iss` to `required_spec_claims` will make it required. |
90 | | /// |
91 | | /// Defaults to `None`. |
92 | | pub iss: Option<HashSet<String>>, |
93 | | /// If it contains a value, the validation will check that the `sub` field is the same as the |
94 | | /// one provided and will error otherwise. |
95 | | /// |
96 | | /// Validation only happens if `sub` claim is present in the token. |
97 | | /// Adding `sub` to `required_spec_claims` will make it required. |
98 | | /// |
99 | | /// Defaults to `None`. |
100 | | pub sub: Option<String>, |
101 | | /// The validation will check that the `alg` of the header is contained |
102 | | /// in the ones provided and will error otherwise. Will error if it is empty. |
103 | | /// |
104 | | /// Defaults to `vec![Algorithm::HS256]`. |
105 | | pub algorithms: Vec<Algorithm>, |
106 | | |
107 | | /// Whether to validate the JWT signature. Very insecure to turn that off |
108 | | pub(crate) validate_signature: bool, |
109 | | } |
110 | | |
111 | | impl Validation { |
112 | | /// Create a default validation setup allowing the given alg |
113 | 0 | pub fn new(alg: Algorithm) -> Validation { |
114 | 0 | let mut required_claims = HashSet::with_capacity(1); |
115 | 0 | required_claims.insert("exp".to_owned()); |
116 | | |
117 | 0 | Validation { |
118 | 0 | required_spec_claims: required_claims, |
119 | 0 | algorithms: vec![alg], |
120 | 0 | leeway: 60, |
121 | 0 | reject_tokens_expiring_in_less_than: 0, |
122 | 0 |
|
123 | 0 | validate_exp: true, |
124 | 0 | validate_nbf: false, |
125 | 0 | validate_aud: true, |
126 | 0 |
|
127 | 0 | iss: None, |
128 | 0 | sub: None, |
129 | 0 | aud: None, |
130 | 0 |
|
131 | 0 | validate_signature: true, |
132 | 0 | } |
133 | 0 | } |
134 | | |
135 | | /// `aud` is a collection of one or more acceptable audience members |
136 | | /// The simple usage is `set_audience(&["some aud name"])` |
137 | 0 | pub fn set_audience<T: ToString>(&mut self, items: &[T]) { |
138 | 0 | self.aud = Some(items.iter().map(|x| x.to_string()).collect()) |
139 | 0 | } |
140 | | |
141 | | /// `iss` is a collection of one or more acceptable issuers members |
142 | | /// The simple usage is `set_issuer(&["some iss name"])` |
143 | 0 | pub fn set_issuer<T: ToString>(&mut self, items: &[T]) { |
144 | 0 | self.iss = Some(items.iter().map(|x| x.to_string()).collect()) |
145 | 0 | } |
146 | | |
147 | | /// Which claims are required to be present for this JWT to be considered valid. |
148 | | /// The only values that will be considered are "exp", "nbf", "aud", "iss", "sub". |
149 | | /// The simple usage is `set_required_spec_claims(&["exp", "nbf"])`. |
150 | | /// If you want to have an empty set, do not use this function - set an empty set on the struct |
151 | | /// param directly. |
152 | 0 | pub fn set_required_spec_claims<T: ToString>(&mut self, items: &[T]) { |
153 | 0 | self.required_spec_claims = items.iter().map(|x| x.to_string()).collect(); |
154 | 0 | } |
155 | | |
156 | | /// Whether to validate the JWT cryptographic signature. |
157 | | /// Disabling validation is dangerous, only do it if you know what you're doing. |
158 | | /// With validation disabled you should not trust any of the values of the claims. |
159 | | #[deprecated( |
160 | | since = "10.1.0", |
161 | | note = "Use `jsonwebtoken::dangerous::insecure_decode` if you require this functionality." |
162 | | )] |
163 | 0 | pub fn insecure_disable_signature_validation(&mut self) { |
164 | 0 | self.validate_signature = false; |
165 | 0 | } |
166 | | } |
167 | | |
168 | | impl Default for Validation { |
169 | 0 | fn default() -> Self { |
170 | 0 | Self::new(Algorithm::HS256) |
171 | 0 | } |
172 | | } |
173 | | |
174 | | /// Gets the current timestamp in the format expected by JWTs. |
175 | | #[cfg(not(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi")))))] |
176 | | #[must_use] |
177 | 0 | pub fn get_current_timestamp() -> u64 { |
178 | 0 | let start = std::time::SystemTime::now(); |
179 | 0 | start.duration_since(std::time::UNIX_EPOCH).expect("Time went backwards").as_secs() |
180 | 0 | } |
181 | | |
182 | | /// Gets the current timestamp in the format expected by JWTs. |
183 | | #[cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))] |
184 | | #[must_use] |
185 | | pub fn get_current_timestamp() -> u64 { |
186 | | js_sys::Date::new_0().get_time() as u64 / 1000 |
187 | | } |
188 | | |
189 | | #[derive(Deserialize)] |
190 | | pub(crate) struct ClaimsForValidation<'a> { |
191 | | #[serde(deserialize_with = "numeric_type", default)] |
192 | | exp: TryParse<u64>, |
193 | | #[serde(deserialize_with = "numeric_type", default)] |
194 | | nbf: TryParse<u64>, |
195 | | #[serde(borrow)] |
196 | | sub: TryParse<Cow<'a, str>>, |
197 | | #[serde(borrow)] |
198 | | iss: TryParse<Issuer<'a>>, |
199 | | #[serde(borrow)] |
200 | | aud: TryParse<Audience<'a>>, |
201 | | } |
202 | | |
203 | | #[derive(Default, Debug)] |
204 | | enum TryParse<T> { |
205 | | Parsed(T), |
206 | | FailedToParse, |
207 | | #[default] |
208 | | NotPresent, |
209 | | } |
210 | | |
211 | | impl<'de, T: Deserialize<'de>> Deserialize<'de> for TryParse<T> { |
212 | 0 | fn deserialize<D: serde::Deserializer<'de>>( |
213 | 0 | deserializer: D, |
214 | 0 | ) -> std::result::Result<Self, D::Error> { |
215 | 0 | Ok(match Option::<T>::deserialize(deserializer) { |
216 | 0 | Ok(Some(value)) => TryParse::Parsed(value), |
217 | 0 | Ok(None) => TryParse::NotPresent, |
218 | 0 | Err(_) => TryParse::FailedToParse, |
219 | | }) |
220 | 0 | } Unexecuted instantiation: <jsonwebtoken::validation::TryParse<alloc::borrow::Cow<str>> as serde_core::de::Deserialize>::deserialize::<serde::private::de::missing_field::MissingFieldDeserializer<serde_json::error::Error>> Unexecuted instantiation: <jsonwebtoken::validation::TryParse<alloc::borrow::Cow<str>> as serde_core::de::Deserialize>::deserialize::<&mut serde_json::de::Deserializer<serde_json::read::SliceRead>> Unexecuted instantiation: <jsonwebtoken::validation::TryParse<jsonwebtoken::validation::Issuer> as serde_core::de::Deserialize>::deserialize::<serde::private::de::missing_field::MissingFieldDeserializer<serde_json::error::Error>> Unexecuted instantiation: <jsonwebtoken::validation::TryParse<jsonwebtoken::validation::Issuer> as serde_core::de::Deserialize>::deserialize::<&mut serde_json::de::Deserializer<serde_json::read::SliceRead>> Unexecuted instantiation: <jsonwebtoken::validation::TryParse<jsonwebtoken::validation::Audience> as serde_core::de::Deserialize>::deserialize::<serde::private::de::missing_field::MissingFieldDeserializer<serde_json::error::Error>> Unexecuted instantiation: <jsonwebtoken::validation::TryParse<jsonwebtoken::validation::Audience> as serde_core::de::Deserialize>::deserialize::<&mut serde_json::de::Deserializer<serde_json::read::SliceRead>> Unexecuted instantiation: <jsonwebtoken::validation::TryParse<_> as serde_core::de::Deserialize>::deserialize::<_> |
221 | | } |
222 | | |
223 | | #[derive(Deserialize)] |
224 | | #[serde(untagged)] |
225 | | enum Audience<'a> { |
226 | | Single(#[serde(borrow)] Cow<'a, str>), |
227 | | Multiple(#[serde(borrow)] HashSet<BorrowedCowIfPossible<'a>>), |
228 | | } |
229 | | |
230 | | #[derive(Deserialize)] |
231 | | #[serde(untagged)] |
232 | | enum Issuer<'a> { |
233 | | Single(#[serde(borrow)] Cow<'a, str>), |
234 | | Multiple(#[serde(borrow)] HashSet<BorrowedCowIfPossible<'a>>), |
235 | | } |
236 | | |
237 | | /// Usually #[serde(borrow)] on `Cow` enables deserializing with no allocations where |
238 | | /// possible (no escapes in the original str) but it does not work on e.g. `HashSet<Cow<str>>` |
239 | | /// We use this struct in this case. |
240 | | #[derive(Deserialize, PartialEq, Eq, Hash)] |
241 | | struct BorrowedCowIfPossible<'a>(#[serde(borrow)] Cow<'a, str>); |
242 | | |
243 | | impl std::borrow::Borrow<str> for BorrowedCowIfPossible<'_> { |
244 | 0 | fn borrow(&self) -> &str { |
245 | 0 | &self.0 |
246 | 0 | } |
247 | | } |
248 | | |
249 | 0 | fn is_subset(reference: &HashSet<String>, given: &HashSet<BorrowedCowIfPossible<'_>>) -> bool { |
250 | | // Check that intersection is non-empty, favoring iterating on smallest |
251 | 0 | if reference.len() < given.len() { |
252 | 0 | reference.iter().any(|a| given.contains(&**a)) |
253 | | } else { |
254 | 0 | given.iter().any(|a| reference.contains(&*a.0)) |
255 | | } |
256 | 0 | } |
257 | | |
258 | 0 | pub(crate) fn validate(claims: ClaimsForValidation, options: &Validation) -> Result<()> { |
259 | 0 | for required_claim in &options.required_spec_claims { |
260 | 0 | let present = match required_claim.as_str() { |
261 | 0 | "exp" => matches!(claims.exp, TryParse::Parsed(_)), |
262 | 0 | "sub" => matches!(claims.sub, TryParse::Parsed(_)), |
263 | 0 | "iss" => matches!(claims.iss, TryParse::Parsed(_)), |
264 | 0 | "aud" => matches!(claims.aud, TryParse::Parsed(_)), |
265 | 0 | "nbf" => matches!(claims.nbf, TryParse::Parsed(_)), |
266 | 0 | _ => continue, |
267 | | }; |
268 | | |
269 | 0 | if !present { |
270 | 0 | return Err(new_error(ErrorKind::MissingRequiredClaim(required_claim.clone()))); |
271 | 0 | } |
272 | | } |
273 | | |
274 | 0 | if options.validate_exp || options.validate_nbf { |
275 | 0 | let now = get_current_timestamp(); |
276 | | |
277 | | // Reject malformed exp/nbf claim when validation is enabled |
278 | 0 | if options.validate_exp && matches!(claims.exp, TryParse::FailedToParse) { |
279 | 0 | return Err(new_error(ErrorKind::InvalidClaimFormat("exp".to_string()))); |
280 | 0 | } |
281 | 0 | if options.validate_nbf && matches!(claims.nbf, TryParse::FailedToParse) { |
282 | 0 | return Err(new_error(ErrorKind::InvalidClaimFormat("nbf".to_string()))); |
283 | 0 | } |
284 | | |
285 | 0 | if matches!(claims.exp, TryParse::Parsed(exp) if exp < options.reject_tokens_expiring_in_less_than) |
286 | | { |
287 | 0 | return Err(new_error(ErrorKind::InvalidToken)); |
288 | 0 | } |
289 | | |
290 | 0 | if matches!(claims.exp, TryParse::Parsed(exp) if options.validate_exp |
291 | 0 | && exp - options.reject_tokens_expiring_in_less_than < now - options.leeway ) |
292 | | { |
293 | 0 | return Err(new_error(ErrorKind::ExpiredSignature)); |
294 | 0 | } |
295 | | |
296 | 0 | if matches!(claims.nbf, TryParse::Parsed(nbf) if options.validate_nbf && nbf > now + options.leeway) |
297 | | { |
298 | 0 | return Err(new_error(ErrorKind::ImmatureSignature)); |
299 | 0 | } |
300 | 0 | } |
301 | | |
302 | 0 | if let (TryParse::Parsed(sub), Some(correct_sub)) = (claims.sub, options.sub.as_deref()) { |
303 | 0 | if sub != correct_sub { |
304 | 0 | return Err(new_error(ErrorKind::InvalidSubject)); |
305 | 0 | } |
306 | 0 | } |
307 | | |
308 | 0 | match (claims.iss, options.iss.as_ref()) { |
309 | 0 | (TryParse::Parsed(Issuer::Single(iss)), Some(correct_iss)) => { |
310 | 0 | if !correct_iss.contains(&*iss) { |
311 | 0 | return Err(new_error(ErrorKind::InvalidIssuer)); |
312 | 0 | } |
313 | | } |
314 | 0 | (TryParse::Parsed(Issuer::Multiple(iss)), Some(correct_iss)) => { |
315 | 0 | if !is_subset(correct_iss, &iss) { |
316 | 0 | return Err(new_error(ErrorKind::InvalidIssuer)); |
317 | 0 | } |
318 | | } |
319 | 0 | _ => {} |
320 | | } |
321 | | |
322 | 0 | if !options.validate_aud { |
323 | 0 | return Ok(()); |
324 | 0 | } |
325 | 0 | match (claims.aud, options.aud.as_ref()) { |
326 | | // Each principal intended to process the JWT MUST |
327 | | // identify itself with a value in the audience claim. If the principal |
328 | | // processing the claim does not identify itself with a value in the |
329 | | // "aud" claim when this claim is present, then the JWT MUST be |
330 | | // rejected. |
331 | 0 | (TryParse::Parsed(Audience::Multiple(aud)), None) => { |
332 | 0 | if !aud.is_empty() { |
333 | 0 | return Err(new_error(ErrorKind::InvalidAudience)); |
334 | 0 | } |
335 | | } |
336 | | (TryParse::Parsed(_), None) => { |
337 | 0 | return Err(new_error(ErrorKind::InvalidAudience)); |
338 | | } |
339 | 0 | (TryParse::Parsed(Audience::Single(aud)), Some(correct_aud)) => { |
340 | 0 | if !correct_aud.contains(&*aud) { |
341 | 0 | return Err(new_error(ErrorKind::InvalidAudience)); |
342 | 0 | } |
343 | | } |
344 | 0 | (TryParse::Parsed(Audience::Multiple(aud)), Some(correct_aud)) => { |
345 | 0 | if !is_subset(correct_aud, &aud) { |
346 | 0 | return Err(new_error(ErrorKind::InvalidAudience)); |
347 | 0 | } |
348 | | } |
349 | 0 | _ => {} |
350 | | } |
351 | | |
352 | 0 | Ok(()) |
353 | 0 | } |
354 | | |
355 | 0 | fn numeric_type<'de, D>(deserializer: D) -> std::result::Result<TryParse<u64>, D::Error> |
356 | 0 | where |
357 | 0 | D: Deserializer<'de>, |
358 | | { |
359 | | struct NumericType(PhantomData<fn() -> TryParse<u64>>); |
360 | | |
361 | | impl Visitor<'_> for NumericType { |
362 | | type Value = TryParse<u64>; |
363 | | |
364 | 0 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
365 | 0 | formatter.write_str("A NumericType that can be reasonably coerced into a u64") |
366 | 0 | } |
367 | | |
368 | 0 | fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E> |
369 | 0 | where |
370 | 0 | E: de::Error, |
371 | | { |
372 | 0 | Ok(TryParse::Parsed(value)) |
373 | 0 | } Unexecuted instantiation: <jsonwebtoken::validation::numeric_type::NumericType as serde_core::de::Visitor>::visit_u64::<serde_json::error::Error> Unexecuted instantiation: <jsonwebtoken::validation::numeric_type::NumericType as serde_core::de::Visitor>::visit_u64::<_> |
374 | | |
375 | 0 | fn visit_f64<E>(self, value: f64) -> std::result::Result<Self::Value, E> |
376 | 0 | where |
377 | 0 | E: de::Error, |
378 | | { |
379 | 0 | if value.is_finite() && value >= 0.0 && value < (u64::MAX as f64) { |
380 | 0 | Ok(TryParse::Parsed(value.round() as u64)) |
381 | | } else { |
382 | 0 | Err(serde::de::Error::custom("NumericType must be representable as a u64")) |
383 | | } |
384 | 0 | } Unexecuted instantiation: <jsonwebtoken::validation::numeric_type::NumericType as serde_core::de::Visitor>::visit_f64::<serde_json::error::Error> Unexecuted instantiation: <jsonwebtoken::validation::numeric_type::NumericType as serde_core::de::Visitor>::visit_f64::<_> |
385 | | } |
386 | | |
387 | 0 | match deserializer.deserialize_any(NumericType(PhantomData)) { |
388 | 0 | Ok(ok) => Ok(ok), |
389 | 0 | Err(_) => Ok(TryParse::FailedToParse), |
390 | | } |
391 | 0 | } Unexecuted instantiation: jsonwebtoken::validation::numeric_type::<&mut serde_json::de::Deserializer<serde_json::read::SliceRead>> Unexecuted instantiation: jsonwebtoken::validation::numeric_type::<_> |
392 | | |
393 | | #[cfg(test)] |
394 | | mod tests { |
395 | | use std::collections::HashSet; |
396 | | |
397 | | use serde_json::json; |
398 | | use wasm_bindgen_test::wasm_bindgen_test; |
399 | | |
400 | | use crate::Algorithm; |
401 | | use crate::errors::ErrorKind; |
402 | | |
403 | | use super::{ClaimsForValidation, Validation, get_current_timestamp, validate}; |
404 | | |
405 | | fn deserialize_claims(claims: &serde_json::Value) -> ClaimsForValidation<'_> { |
406 | | serde::Deserialize::deserialize(claims).unwrap() |
407 | | } |
408 | | |
409 | | #[test] |
410 | | #[wasm_bindgen_test] |
411 | | fn exp_in_future_ok() { |
412 | | let claims = json!({ "exp": get_current_timestamp() + 10000 }); |
413 | | let res = validate(deserialize_claims(&claims), &Validation::new(Algorithm::HS256)); |
414 | | assert!(res.is_ok()); |
415 | | } |
416 | | |
417 | | #[test] |
418 | | #[wasm_bindgen_test] |
419 | | fn exp_in_future_but_in_rejection_period_fails() { |
420 | | let claims = json!({ "exp": get_current_timestamp() + 500 }); |
421 | | let mut validation = Validation::new(Algorithm::HS256); |
422 | | validation.leeway = 0; |
423 | | validation.reject_tokens_expiring_in_less_than = 501; |
424 | | let res = validate(deserialize_claims(&claims), &validation); |
425 | | assert!(res.is_err()); |
426 | | } |
427 | | |
428 | | #[test] |
429 | | #[wasm_bindgen_test] |
430 | | fn exp_float_in_future_ok() { |
431 | | let claims = json!({ "exp": (get_current_timestamp() as f64) + 10000.123 }); |
432 | | let res = validate(deserialize_claims(&claims), &Validation::new(Algorithm::HS256)); |
433 | | assert!(res.is_ok()); |
434 | | } |
435 | | |
436 | | #[test] |
437 | | #[wasm_bindgen_test] |
438 | | fn exp_float_in_future_but_in_rejection_period_fails() { |
439 | | let claims = json!({ "exp": (get_current_timestamp() as f64) + 500.123 }); |
440 | | let mut validation = Validation::new(Algorithm::HS256); |
441 | | validation.leeway = 0; |
442 | | validation.reject_tokens_expiring_in_less_than = 501; |
443 | | let res = validate(deserialize_claims(&claims), &validation); |
444 | | assert!(res.is_err()); |
445 | | } |
446 | | |
447 | | #[test] |
448 | | #[wasm_bindgen_test] |
449 | | fn exp_in_past_fails() { |
450 | | let claims = json!({ "exp": get_current_timestamp() - 100000 }); |
451 | | let res = validate(deserialize_claims(&claims), &Validation::new(Algorithm::HS256)); |
452 | | assert!(res.is_err()); |
453 | | |
454 | | match res.unwrap_err().kind() { |
455 | | ErrorKind::ExpiredSignature => (), |
456 | | _ => unreachable!(), |
457 | | }; |
458 | | } |
459 | | |
460 | | #[test] |
461 | | #[wasm_bindgen_test] |
462 | | fn exp_float_in_past_fails() { |
463 | | let claims = json!({ "exp": (get_current_timestamp() as f64) - 100000.1234 }); |
464 | | let res = validate(deserialize_claims(&claims), &Validation::new(Algorithm::HS256)); |
465 | | assert!(res.is_err()); |
466 | | |
467 | | match res.unwrap_err().kind() { |
468 | | ErrorKind::ExpiredSignature => (), |
469 | | _ => unreachable!(), |
470 | | }; |
471 | | } |
472 | | |
473 | | #[test] |
474 | | #[wasm_bindgen_test] |
475 | | fn exp_in_past_but_in_leeway_ok() { |
476 | | let claims = json!({ "exp": get_current_timestamp() - 500 }); |
477 | | let mut validation = Validation::new(Algorithm::HS256); |
478 | | validation.leeway = 1000 * 60; |
479 | | let res = validate(deserialize_claims(&claims), &validation); |
480 | | assert!(res.is_ok()); |
481 | | } |
482 | | |
483 | | // https://github.com/Keats/jsonwebtoken/issues/51 |
484 | | #[test] |
485 | | #[wasm_bindgen_test] |
486 | | fn validate_required_fields_are_present() { |
487 | | for spec_claim in ["exp", "nbf", "aud", "iss", "sub"] { |
488 | | let claims = json!({}); |
489 | | let mut validation = Validation::new(Algorithm::HS256); |
490 | | validation.set_required_spec_claims(&[spec_claim]); |
491 | | let res = validate(deserialize_claims(&claims), &validation).unwrap_err(); |
492 | | assert_eq!(res.kind(), &ErrorKind::MissingRequiredClaim(spec_claim.to_owned())); |
493 | | } |
494 | | } |
495 | | |
496 | | #[test] |
497 | | #[wasm_bindgen_test] |
498 | | fn exp_validated_but_not_required_ok() { |
499 | | let claims = json!({}); |
500 | | let mut validation = Validation::new(Algorithm::HS256); |
501 | | validation.required_spec_claims = HashSet::new(); |
502 | | validation.validate_exp = true; |
503 | | let res = validate(deserialize_claims(&claims), &validation); |
504 | | assert!(res.is_ok()); |
505 | | } |
506 | | |
507 | | #[test] |
508 | | #[wasm_bindgen_test] |
509 | | fn exp_validated_but_not_required_fails() { |
510 | | let claims = json!({ "exp": (get_current_timestamp() as f64) - 100000.1234 }); |
511 | | let mut validation = Validation::new(Algorithm::HS256); |
512 | | validation.required_spec_claims = HashSet::new(); |
513 | | validation.validate_exp = true; |
514 | | let res = validate(deserialize_claims(&claims), &validation); |
515 | | assert!(res.is_err()); |
516 | | } |
517 | | |
518 | | #[test] |
519 | | #[wasm_bindgen_test] |
520 | | fn exp_required_but_not_validated_ok() { |
521 | | let claims = json!({ "exp": (get_current_timestamp() as f64) - 100000.1234 }); |
522 | | let mut validation = Validation::new(Algorithm::HS256); |
523 | | validation.set_required_spec_claims(&["exp"]); |
524 | | validation.validate_exp = false; |
525 | | let res = validate(deserialize_claims(&claims), &validation); |
526 | | assert!(res.is_ok()); |
527 | | } |
528 | | |
529 | | #[test] |
530 | | #[wasm_bindgen_test] |
531 | | fn exp_required_but_not_validated_fails() { |
532 | | let claims = json!({}); |
533 | | let mut validation = Validation::new(Algorithm::HS256); |
534 | | validation.set_required_spec_claims(&["exp"]); |
535 | | validation.validate_exp = false; |
536 | | let res = validate(deserialize_claims(&claims), &validation); |
537 | | assert!(res.is_err()); |
538 | | } |
539 | | |
540 | | #[test] |
541 | | #[wasm_bindgen_test] |
542 | | fn nbf_in_past_ok() { |
543 | | let claims = json!({ "nbf": get_current_timestamp() - 10000 }); |
544 | | let mut validation = Validation::new(Algorithm::HS256); |
545 | | validation.required_spec_claims = HashSet::new(); |
546 | | validation.validate_exp = false; |
547 | | validation.validate_nbf = true; |
548 | | let res = validate(deserialize_claims(&claims), &validation); |
549 | | assert!(res.is_ok()); |
550 | | } |
551 | | |
552 | | #[test] |
553 | | #[wasm_bindgen_test] |
554 | | fn nbf_float_in_past_ok() { |
555 | | let claims = json!({ "nbf": (get_current_timestamp() as f64) - 10000.1234 }); |
556 | | let mut validation = Validation::new(Algorithm::HS256); |
557 | | validation.required_spec_claims = HashSet::new(); |
558 | | validation.validate_exp = false; |
559 | | validation.validate_nbf = true; |
560 | | let res = validate(deserialize_claims(&claims), &validation); |
561 | | assert!(res.is_ok()); |
562 | | } |
563 | | |
564 | | #[test] |
565 | | #[wasm_bindgen_test] |
566 | | fn nbf_in_future_fails() { |
567 | | let claims = json!({ "nbf": get_current_timestamp() + 100000 }); |
568 | | let mut validation = Validation::new(Algorithm::HS256); |
569 | | validation.required_spec_claims = HashSet::new(); |
570 | | validation.validate_exp = false; |
571 | | validation.validate_nbf = true; |
572 | | let res = validate(deserialize_claims(&claims), &validation); |
573 | | assert!(res.is_err()); |
574 | | |
575 | | match res.unwrap_err().kind() { |
576 | | ErrorKind::ImmatureSignature => (), |
577 | | _ => unreachable!(), |
578 | | }; |
579 | | } |
580 | | |
581 | | #[test] |
582 | | #[wasm_bindgen_test] |
583 | | fn nbf_in_future_but_in_leeway_ok() { |
584 | | let claims = json!({ "nbf": get_current_timestamp() + 500 }); |
585 | | let mut validation = Validation::new(Algorithm::HS256); |
586 | | validation.required_spec_claims = HashSet::new(); |
587 | | validation.validate_exp = false; |
588 | | validation.validate_nbf = true; |
589 | | validation.leeway = 1000 * 60; |
590 | | let res = validate(deserialize_claims(&claims), &validation); |
591 | | assert!(res.is_ok()); |
592 | | } |
593 | | |
594 | | #[test] |
595 | | #[wasm_bindgen_test] |
596 | | fn iss_string_ok() { |
597 | | let claims = json!({"iss": ["Keats"]}); |
598 | | let mut validation = Validation::new(Algorithm::HS256); |
599 | | validation.required_spec_claims = HashSet::new(); |
600 | | validation.validate_exp = false; |
601 | | validation.set_issuer(&["Keats"]); |
602 | | let res = validate(deserialize_claims(&claims), &validation); |
603 | | assert!(res.is_ok()); |
604 | | } |
605 | | |
606 | | #[test] |
607 | | #[wasm_bindgen_test] |
608 | | fn iss_array_of_string_ok() { |
609 | | let claims = json!({"iss": ["UserA", "UserB"]}); |
610 | | let mut validation = Validation::new(Algorithm::HS256); |
611 | | validation.required_spec_claims = HashSet::new(); |
612 | | validation.validate_exp = false; |
613 | | validation.set_issuer(&["UserA", "UserB"]); |
614 | | let res = validate(deserialize_claims(&claims), &validation); |
615 | | assert!(res.is_ok()); |
616 | | } |
617 | | |
618 | | #[test] |
619 | | #[wasm_bindgen_test] |
620 | | fn iss_not_matching_fails() { |
621 | | let claims = json!({"iss": "Hacked"}); |
622 | | |
623 | | let mut validation = Validation::new(Algorithm::HS256); |
624 | | validation.required_spec_claims = HashSet::new(); |
625 | | validation.validate_exp = false; |
626 | | validation.set_issuer(&["Keats"]); |
627 | | let res = validate(deserialize_claims(&claims), &validation); |
628 | | assert!(res.is_err()); |
629 | | |
630 | | match res.unwrap_err().kind() { |
631 | | ErrorKind::InvalidIssuer => (), |
632 | | _ => unreachable!(), |
633 | | }; |
634 | | } |
635 | | |
636 | | #[test] |
637 | | #[wasm_bindgen_test] |
638 | | fn iss_missing_fails() { |
639 | | let claims = json!({}); |
640 | | |
641 | | let mut validation = Validation::new(Algorithm::HS256); |
642 | | validation.set_required_spec_claims(&["iss"]); |
643 | | validation.validate_exp = false; |
644 | | validation.set_issuer(&["Keats"]); |
645 | | let res = validate(deserialize_claims(&claims), &validation); |
646 | | |
647 | | match res.unwrap_err().kind() { |
648 | | ErrorKind::MissingRequiredClaim(claim) => assert_eq!(claim, "iss"), |
649 | | _ => unreachable!(), |
650 | | }; |
651 | | } |
652 | | |
653 | | #[test] |
654 | | #[wasm_bindgen_test] |
655 | | fn sub_ok() { |
656 | | let claims = json!({"sub": "Keats"}); |
657 | | let mut validation = Validation::new(Algorithm::HS256); |
658 | | validation.required_spec_claims = HashSet::new(); |
659 | | validation.validate_exp = false; |
660 | | validation.sub = Some("Keats".to_owned()); |
661 | | let res = validate(deserialize_claims(&claims), &validation); |
662 | | assert!(res.is_ok()); |
663 | | } |
664 | | |
665 | | #[test] |
666 | | #[wasm_bindgen_test] |
667 | | fn sub_not_matching_fails() { |
668 | | let claims = json!({"sub": "Hacked"}); |
669 | | let mut validation = Validation::new(Algorithm::HS256); |
670 | | validation.required_spec_claims = HashSet::new(); |
671 | | validation.validate_exp = false; |
672 | | validation.sub = Some("Keats".to_owned()); |
673 | | let res = validate(deserialize_claims(&claims), &validation); |
674 | | assert!(res.is_err()); |
675 | | |
676 | | match res.unwrap_err().kind() { |
677 | | ErrorKind::InvalidSubject => (), |
678 | | _ => unreachable!(), |
679 | | }; |
680 | | } |
681 | | |
682 | | #[test] |
683 | | #[wasm_bindgen_test] |
684 | | fn sub_missing_fails() { |
685 | | let claims = json!({}); |
686 | | let mut validation = Validation::new(Algorithm::HS256); |
687 | | validation.validate_exp = false; |
688 | | validation.set_required_spec_claims(&["sub"]); |
689 | | validation.sub = Some("Keats".to_owned()); |
690 | | let res = validate(deserialize_claims(&claims), &validation); |
691 | | assert!(res.is_err()); |
692 | | |
693 | | match res.unwrap_err().kind() { |
694 | | ErrorKind::MissingRequiredClaim(claim) => assert_eq!(claim, "sub"), |
695 | | _ => unreachable!(), |
696 | | }; |
697 | | } |
698 | | |
699 | | #[test] |
700 | | #[wasm_bindgen_test] |
701 | | fn aud_string_ok() { |
702 | | let claims = json!({"aud": "Everyone"}); |
703 | | let mut validation = Validation::new(Algorithm::HS256); |
704 | | validation.validate_exp = false; |
705 | | validation.required_spec_claims = HashSet::new(); |
706 | | validation.set_audience(&["Everyone"]); |
707 | | let res = validate(deserialize_claims(&claims), &validation); |
708 | | assert!(res.is_ok()); |
709 | | } |
710 | | |
711 | | #[test] |
712 | | #[wasm_bindgen_test] |
713 | | fn aud_array_of_string_ok() { |
714 | | let claims = json!({"aud": ["UserA", "UserB"]}); |
715 | | let mut validation = Validation::new(Algorithm::HS256); |
716 | | validation.validate_exp = false; |
717 | | validation.required_spec_claims = HashSet::new(); |
718 | | validation.set_audience(&["UserA", "UserB"]); |
719 | | let res = validate(deserialize_claims(&claims), &validation); |
720 | | assert!(res.is_ok()); |
721 | | } |
722 | | |
723 | | #[test] |
724 | | #[wasm_bindgen_test] |
725 | | fn aud_type_mismatch_fails() { |
726 | | let claims = json!({"aud": ["Everyone"]}); |
727 | | let mut validation = Validation::new(Algorithm::HS256); |
728 | | validation.validate_exp = false; |
729 | | validation.required_spec_claims = HashSet::new(); |
730 | | validation.set_audience(&["UserA", "UserB"]); |
731 | | let res = validate(deserialize_claims(&claims), &validation); |
732 | | assert!(res.is_err()); |
733 | | |
734 | | match res.unwrap_err().kind() { |
735 | | ErrorKind::InvalidAudience => (), |
736 | | _ => unreachable!(), |
737 | | }; |
738 | | } |
739 | | |
740 | | #[test] |
741 | | #[wasm_bindgen_test] |
742 | | fn aud_correct_type_not_matching_fails() { |
743 | | let claims = json!({"aud": ["Everyone"]}); |
744 | | let mut validation = Validation::new(Algorithm::HS256); |
745 | | validation.validate_exp = false; |
746 | | validation.required_spec_claims = HashSet::new(); |
747 | | validation.set_audience(&["None"]); |
748 | | let res = validate(deserialize_claims(&claims), &validation); |
749 | | assert!(res.is_err()); |
750 | | |
751 | | match res.unwrap_err().kind() { |
752 | | ErrorKind::InvalidAudience => (), |
753 | | _ => unreachable!(), |
754 | | }; |
755 | | } |
756 | | |
757 | | #[test] |
758 | | #[wasm_bindgen_test] |
759 | | fn aud_none_fails() { |
760 | | let claims = json!({"aud": ["Everyone"]}); |
761 | | let mut validation = Validation::new(Algorithm::HS256); |
762 | | validation.validate_exp = false; |
763 | | validation.required_spec_claims = HashSet::new(); |
764 | | validation.aud = None; |
765 | | let res = validate(deserialize_claims(&claims), &validation); |
766 | | assert!(res.is_err()); |
767 | | |
768 | | match res.unwrap_err().kind() { |
769 | | ErrorKind::InvalidAudience => (), |
770 | | _ => unreachable!(), |
771 | | }; |
772 | | } |
773 | | |
774 | | #[test] |
775 | | #[wasm_bindgen_test] |
776 | | fn aud_validation_skipped() { |
777 | | let claims = json!({"aud": ["Everyone"]}); |
778 | | let mut validation = Validation::new(Algorithm::HS256); |
779 | | validation.validate_exp = false; |
780 | | validation.validate_aud = false; |
781 | | validation.required_spec_claims = HashSet::new(); |
782 | | validation.aud = None; |
783 | | let res = validate(deserialize_claims(&claims), &validation); |
784 | | assert!(res.is_ok()); |
785 | | } |
786 | | |
787 | | #[test] |
788 | | #[wasm_bindgen_test] |
789 | | fn aud_missing_fails() { |
790 | | let claims = json!({}); |
791 | | let mut validation = Validation::new(Algorithm::HS256); |
792 | | validation.validate_exp = false; |
793 | | validation.set_required_spec_claims(&["aud"]); |
794 | | validation.set_audience(&["None"]); |
795 | | let res = validate(deserialize_claims(&claims), &validation); |
796 | | assert!(res.is_err()); |
797 | | |
798 | | match res.unwrap_err().kind() { |
799 | | ErrorKind::MissingRequiredClaim(claim) => assert_eq!(claim, "aud"), |
800 | | _ => unreachable!(), |
801 | | }; |
802 | | } |
803 | | |
804 | | // https://github.com/Keats/jsonwebtoken/issues/51 |
805 | | #[test] |
806 | | #[wasm_bindgen_test] |
807 | | fn does_validation_in_right_order() { |
808 | | let claims = json!({ "exp": get_current_timestamp() + 10000 }); |
809 | | |
810 | | let mut validation = Validation::new(Algorithm::HS256); |
811 | | validation.set_required_spec_claims(&["exp", "iss"]); |
812 | | validation.leeway = 5; |
813 | | validation.set_issuer(&["iss no check"]); |
814 | | validation.set_audience(&["iss no check"]); |
815 | | |
816 | | let res = validate(deserialize_claims(&claims), &validation); |
817 | | // It errors because it needs to validate iss/sub which are missing |
818 | | assert!(res.is_err()); |
819 | | match res.unwrap_err().kind() { |
820 | | ErrorKind::MissingRequiredClaim(claim) => assert_eq!(claim, "iss"), |
821 | | t => panic!("{:?}", t), |
822 | | }; |
823 | | } |
824 | | |
825 | | // https://github.com/Keats/jsonwebtoken/issues/110 |
826 | | #[test] |
827 | | #[wasm_bindgen_test] |
828 | | fn aud_use_validation_struct() { |
829 | | let claims = json!({"aud": "my-googleclientid1234.apps.googleusercontent.com"}); |
830 | | |
831 | | let aud = "my-googleclientid1234.apps.googleusercontent.com".to_string(); |
832 | | let mut aud_hashset = std::collections::HashSet::new(); |
833 | | aud_hashset.insert(aud); |
834 | | let mut validation = Validation::new(Algorithm::HS256); |
835 | | validation.validate_exp = false; |
836 | | validation.required_spec_claims = HashSet::new(); |
837 | | validation.set_audience(&["my-googleclientid1234.apps.googleusercontent.com"]); |
838 | | |
839 | | let res = validate(deserialize_claims(&claims), &validation); |
840 | | assert!(res.is_ok()); |
841 | | } |
842 | | |
843 | | // https://github.com/Keats/jsonwebtoken/issues/388 |
844 | | #[test] |
845 | | #[wasm_bindgen_test] |
846 | | fn doesnt_panic_with_leeway_overflow() { |
847 | | let claims = json!({ "exp": 1 }); |
848 | | |
849 | | let mut validation = Validation::new(Algorithm::HS256); |
850 | | validation.reject_tokens_expiring_in_less_than = 100; |
851 | | |
852 | | let res = validate(deserialize_claims(&claims), &validation); |
853 | | assert!(res.is_err()); |
854 | | } |
855 | | } |