use std::collections::HashSet; use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::map::Map; use serde_json::Value; use crate::algorithms::Algorithm; use crate::errors::{new_error, ErrorKind, Result}; /// Contains the various validations that are applied after decoding a JWT. /// /// All time validation happen on UTC timestamps as seconds. /// /// ```rust /// use jsonwebtoken::{Validation, Algorithm}; /// /// let mut validation = Validation::new(Algorithm::HS256); /// validation.leeway = 5; /// // Setting audience /// validation.set_audience(&["Me"]); // a single string /// validation.set_audience(&["Me", "You"]); // array of strings /// // or issuer /// validation.set_iss(&["Me"]); // a single string /// validation.set_iss(&["Me", "You"]); // array of strings /// ``` #[derive(Debug, Clone, PartialEq)] pub struct Validation { /// Add some leeway (in seconds) to the `exp`, `iat` and `nbf` validation to /// account for clock skew. /// /// Defaults to `0`. pub leeway: u64, /// Whether to validate the `exp` field. /// /// It will return an error if the time in the `exp` field is past. /// /// Defaults to `true`. pub validate_exp: bool, /// Whether to validate the `nbf` field. /// /// It will return an error if the current timestamp is before the time in the `nbf` field. /// /// Defaults to `false`. pub validate_nbf: bool, /// If it contains a value, the validation will check that the `aud` field is a member of the /// audience provided and will error otherwise. /// /// Defaults to `None`. pub aud: Option>, /// If it contains a value, the validation will check that the `iss` field is a member of the /// iss provided and will error otherwise. /// /// Defaults to `None`. pub iss: Option>, /// If it contains a value, the validation will check that the `sub` field is the same as the /// one provided and will error otherwise. /// /// Defaults to `None`. pub sub: Option, /// The validation will check that the `alg` of the header is contained /// in the ones provided and will error otherwise. Will error if it is empty. /// /// Defaults to `vec![Algorithm::HS256]`. pub algorithms: Vec, /// Whether to validate the JWT signature. Very insecure to turn that off pub(crate) validate_signature: bool, } impl Validation { /// Create a default validation setup allowing the given alg pub fn new(alg: Algorithm) -> Validation { Validation { algorithms: vec![alg], leeway: 0, validate_exp: true, validate_nbf: false, iss: None, sub: None, aud: None, validate_signature: true, } } /// `aud` is a collection of one or more acceptable audience members pub fn set_audience(&mut self, items: &[T]) { self.aud = Some(items.iter().map(|x| x.to_string()).collect()) } /// `iss` is a collection of one or more acceptable iss members pub fn set_iss(&mut self, items: &[T]) { self.iss = Some(items.iter().map(|x| x.to_string()).collect()) } /// Whether to validate the JWT cryptographic signature /// Very insecure to turn that off, only do it if you know what you're doing. /// With this flag turned off, you should not trust any of the values of the claims. pub fn insecure_disable_signature_validation(&mut self) { self.validate_signature = false; } } /// Gets the current timestamp in the format JWT expect pub fn get_current_timestamp() -> u64 { let start = SystemTime::now(); start.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() } pub fn validate(claims: &Map, options: &Validation) -> Result<()> { let now = get_current_timestamp(); if options.validate_exp { if let Some(exp) = claims.get("exp") { if let Some(exp) = exp.as_u64() { if exp < now - options.leeway { return Err(new_error(ErrorKind::ExpiredSignature)); } } else { return Err(new_error(ErrorKind::ExpiredSignature)); } } else { return Err(new_error(ErrorKind::ExpiredSignature)); } } if options.validate_nbf { if let Some(nbf) = claims.get("nbf") { if let Some(nbf) = nbf.as_u64() { if nbf > now + options.leeway { return Err(new_error(ErrorKind::ImmatureSignature)); } } else { return Err(new_error(ErrorKind::ImmatureSignature)); } } else { return Err(new_error(ErrorKind::ImmatureSignature)); } } if let Some(ref correct_sub) = options.sub { if let Some(Value::String(sub)) = claims.get("sub") { if sub != correct_sub { return Err(new_error(ErrorKind::InvalidSubject)); } } else { return Err(new_error(ErrorKind::InvalidSubject)); } } if let Some(ref correct_iss) = options.iss { if let Some(Value::String(iss)) = claims.get("iss") { if !correct_iss.contains(iss) { return Err(new_error(ErrorKind::InvalidIssuer)); } } else { return Err(new_error(ErrorKind::InvalidIssuer)); } } if let Some(ref correct_aud) = options.aud { if let Some(aud) = claims.get("aud") { match aud { Value::String(aud) => { if !correct_aud.contains(aud) { return Err(new_error(ErrorKind::InvalidAudience)); } } Value::Array(_) => { use serde::Deserialize; let aud = HashSet::::deserialize(aud)?; if aud.intersection(correct_aud).next().is_none() { return Err(new_error(ErrorKind::InvalidAudience)); } } _ => return Err(new_error(ErrorKind::InvalidAudience)), }; } else { return Err(new_error(ErrorKind::InvalidAudience)); } } Ok(()) } #[cfg(test)] mod tests { use serde_json::map::Map; use serde_json::to_value; use super::{get_current_timestamp, validate, Validation}; use crate::errors::ErrorKind; use crate::Algorithm; #[test] fn exp_in_future_ok() { let mut claims = Map::new(); claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap()); let res = validate(&claims, &Validation::new(Algorithm::HS256)); assert!(res.is_ok()); } #[test] fn exp_in_past_fails() { let mut claims = Map::new(); claims.insert("exp".to_string(), to_value(get_current_timestamp() - 100000).unwrap()); let res = validate(&claims, &Validation::new(Algorithm::HS256)); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::ExpiredSignature => (), _ => unreachable!(), }; } #[test] fn exp_in_past_but_in_leeway_ok() { let mut claims = Map::new(); claims.insert("exp".to_string(), to_value(get_current_timestamp() - 500).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.leeway = 1000 * 60; let res = validate(&claims, &validation); assert!(res.is_ok()); } // https://github.com/Keats/jsonwebtoken/issues/51 #[test] fn validation_called_even_if_field_is_empty() { let claims = Map::new(); let res = validate(&claims, &Validation::new(Algorithm::HS256)); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::ExpiredSignature => (), _ => unreachable!(), }; } #[test] fn nbf_in_past_ok() { let mut claims = Map::new(); claims.insert("nbf".to_string(), to_value(get_current_timestamp() - 10000).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.validate_nbf = true; let res = validate(&claims, &validation); assert!(res.is_ok()); } #[test] fn nbf_in_future_fails() { let mut claims = Map::new(); claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 100000).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.validate_nbf = true; let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::ImmatureSignature => (), _ => unreachable!(), }; } #[test] fn nbf_in_future_but_in_leeway_ok() { let mut claims = Map::new(); claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 500).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.validate_nbf = true; validation.leeway = 1000 * 60; let res = validate(&claims, &validation); assert!(res.is_ok()); } #[test] fn iss_ok() { let mut claims = Map::new(); claims.insert("iss".to_string(), to_value("Keats").unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_iss(&["Keats"]); let res = validate(&claims, &validation); assert!(res.is_ok()); } #[test] fn iss_not_matching_fails() { let mut claims = Map::new(); claims.insert("iss".to_string(), to_value("Hacked").unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_iss(&["Keats"]); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidIssuer => (), _ => unreachable!(), }; } #[test] fn iss_missing_fails() { let claims = Map::new(); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_iss(&["Keats"]); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidIssuer => (), _ => unreachable!(), }; } #[test] fn sub_ok() { let mut claims = Map::new(); claims.insert("sub".to_string(), to_value("Keats").unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.sub = Some("Keats".to_owned()); let res = validate(&claims, &validation); assert!(res.is_ok()); } #[test] fn sub_not_matching_fails() { let mut claims = Map::new(); claims.insert("sub".to_string(), to_value("Hacked").unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.sub = Some("Keats".to_owned()); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidSubject => (), _ => unreachable!(), }; } #[test] fn sub_missing_fails() { let claims = Map::new(); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.sub = Some("Keats".to_owned()); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidSubject => (), _ => unreachable!(), }; } #[test] fn aud_string_ok() { let mut claims = Map::new(); claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_audience(&["Everyone"]); let res = validate(&claims, &validation); assert!(res.is_ok()); } #[test] fn aud_array_of_string_ok() { let mut claims = Map::new(); claims.insert("aud".to_string(), to_value(["UserA", "UserB"]).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_audience(&["UserA", "UserB"]); let res = validate(&claims, &validation); assert!(res.is_ok()); } #[test] fn aud_type_mismatch_fails() { let mut claims = Map::new(); claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_audience(&["UserA", "UserB"]); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidAudience => (), _ => unreachable!(), }; } #[test] fn aud_correct_type_not_matching_fails() { let mut claims = Map::new(); claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_audience(&["None"]); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidAudience => (), _ => unreachable!(), }; } #[test] fn aud_missing_fails() { let claims = Map::new(); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_audience(&["None"]); let res = validate(&claims, &validation); assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidAudience => (), _ => unreachable!(), }; } // https://github.com/Keats/jsonwebtoken/issues/51 #[test] fn does_validation_in_right_order() { let mut claims = Map::new(); claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap()); let mut validation = Validation::new(Algorithm::HS256); validation.leeway = 5; validation.set_iss(&["iss no check"]); validation.set_audience(&["iss no check"]); let res = validate(&claims, &validation); // It errors because it needs to validate iss/sub which are missing assert!(res.is_err()); match res.unwrap_err().kind() { ErrorKind::InvalidIssuer => (), t => panic!("{:?}", t), }; } // https://github.com/Keats/jsonwebtoken/issues/110 #[test] fn aud_use_validation_struct() { let mut claims = Map::new(); claims.insert( "aud".to_string(), to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap(), ); let aud = "my-googleclientid1234.apps.googleusercontent.com".to_string(); let mut aud_hashset = std::collections::HashSet::new(); aud_hashset.insert(aud); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_audience(&["my-googleclientid1234.apps.googleusercontent.com"]); let res = validate(&claims, &validation); println!("{:?}", res); assert!(res.is_ok()); } }