Fix validation not working properly

Close #51
This commit is contained in:
Vincent Prouillet 2018-07-25 15:42:00 +02:00
parent 109978ab6b
commit 5528497f5a
6 changed files with 178 additions and 49 deletions

View File

@ -4,6 +4,8 @@
- Update ring
- Change error handling to be based on simple struct/enum rather than error-chain
- Fix validations not being called properly in some cases
- Default validation is not checking `iat` and `nbf` anymore
## 4.0.1 (2018-03-19)

View File

@ -26,6 +26,12 @@ pub enum Algorithm {
RS512,
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HS256
}
}
/// The actual HS signing + encoding
fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
let signing_key = hmac::SigningKey::new(alg, key);
@ -112,9 +118,3 @@ pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algor
Algorithm::RS512 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key),
}
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HS256
}
}

View File

@ -114,10 +114,9 @@ pub fn decode<T: DeserializeOwned>(token: &str, key: &[u8], validation: &Validat
}
let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?;
validate(&claims_map, validation)?;
Ok(TokenData { header: header, claims: decoded_claims })
Ok(TokenData { header, claims: decoded_claims })
}
/// Decode a token without any signature validation into a struct containing 2 fields: `claims` and `header`.

View File

@ -95,8 +95,8 @@ impl Default for Validation {
leeway: 0,
validate_exp: true,
validate_iat: true,
validate_nbf: true,
validate_iat: false,
validate_nbf: false,
iss: None,
sub: None,
@ -112,45 +112,63 @@ impl Default for Validation {
pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()> {
let now = Utc::now().timestamp();
if let Some(iat) = claims.get("iat") {
if options.validate_iat && from_value::<i64>(iat.clone())? > now + options.leeway {
if options.validate_iat {
if let Some(iat) = claims.get("iat") {
if from_value::<i64>(iat.clone())? > now + options.leeway {
return Err(new_error(ErrorKind::InvalidIssuedAt));
}
} else {
return Err(new_error(ErrorKind::InvalidIssuedAt));
}
}
if let Some(exp) = claims.get("exp") {
if options.validate_exp && from_value::<i64>(exp.clone())? < now - options.leeway {
if options.validate_exp {
if let Some(exp) = claims.get("exp") {
if from_value::<i64>(exp.clone())? < now - options.leeway {
return Err(new_error(ErrorKind::ExpiredSignature));
}
} else {
return Err(new_error(ErrorKind::ExpiredSignature));
}
}
if let Some(nbf) = claims.get("nbf") {
if options.validate_nbf && from_value::<i64>(nbf.clone())? > now + options.leeway {
if options.validate_nbf {
if let Some(nbf) = claims.get("nbf") {
if from_value::<i64>(nbf.clone())? > now + options.leeway {
return Err(new_error(ErrorKind::ImmatureSignature));
}
} else {
return Err(new_error(ErrorKind::ImmatureSignature));
}
}
if let Some(iss) = claims.get("iss") {
if let Some(ref correct_iss) = options.iss {
if let Some(ref correct_iss) = options.iss {
if let Some(iss) = claims.get("iss") {
if from_value::<String>(iss.clone())? != *correct_iss {
return Err(new_error(ErrorKind::InvalidIssuer));
}
} else {
return Err(new_error(ErrorKind::InvalidIssuer));
}
}
if let Some(sub) = claims.get("sub") {
if let Some(ref correct_sub) = options.sub {
if let Some(ref correct_sub) = options.sub {
if let Some(sub) = claims.get("sub") {
if from_value::<String>(sub.clone())? != *correct_sub {
return Err(new_error(ErrorKind::InvalidSubject));
}
} else {
return Err(new_error(ErrorKind::InvalidSubject));
}
}
if let Some(aud) = claims.get("aud") {
if let Some(ref correct_aud) = options.aud {
if let Some(ref correct_aud) = options.aud {
if let Some(aud) = claims.get("aud") {
if aud != correct_aud {
return Err(new_error(ErrorKind::InvalidAudience));
}
} else {
return Err(new_error(ErrorKind::InvalidAudience));
}
}
@ -172,7 +190,8 @@ mod tests {
fn iat_in_past_ok() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
let res = validate(&claims, &Validation::default());
let validation = Validation { validate_exp: false, validate_iat: true, ..Validation::default() };
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
@ -180,7 +199,8 @@ mod tests {
fn iat_in_future_fails() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
let res = validate(&claims, &Validation::default());
let validation = Validation { validate_exp: false, validate_iat: true, ..Validation::default() };
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
@ -195,6 +215,8 @@ mod tests {
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 50).unwrap());
let validation = Validation {
leeway: 1000 * 60,
validate_iat: true,
validate_exp: false,
..Default::default()
};
let res = validate(&claims, &validation);
@ -234,11 +256,24 @@ mod tests {
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::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::ExpiredSignature => (),
_ => assert!(false),
};
}
#[test]
fn nbf_in_past_ok() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
let res = validate(&claims, &Validation::default());
let validation = Validation { validate_exp: false, validate_nbf: true, ..Validation::default() };
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
@ -246,7 +281,8 @@ mod tests {
fn nbf_in_future_fails() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
let res = validate(&claims, &Validation::default());
let validation = Validation { validate_exp: false, validate_nbf: true, ..Validation::default() };
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
@ -261,6 +297,8 @@ mod tests {
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 500).unwrap());
let validation = Validation {
leeway: 1000 * 60,
validate_nbf: true,
validate_exp: false,
..Default::default()
};
let res = validate(&claims, &validation);
@ -272,6 +310,7 @@ mod tests {
let mut claims = Map::new();
claims.insert("iss".to_string(), to_value("Keats").unwrap());
let validation = Validation {
validate_exp: false,
iss: Some("Keats".to_string()),
..Default::default()
};
@ -284,6 +323,24 @@ mod tests {
let mut claims = Map::new();
claims.insert("iss".to_string(), to_value("Hacked").unwrap());
let validation = Validation {
validate_exp: false,
iss: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidIssuer => (),
_ => assert!(false),
};
}
#[test]
fn iss_missing_fails() {
let claims = Map::new();
let validation = Validation {
validate_exp: false,
iss: Some("Keats".to_string()),
..Default::default()
};
@ -301,6 +358,7 @@ mod tests {
let mut claims = Map::new();
claims.insert("sub".to_string(), to_value("Keats").unwrap());
let validation = Validation {
validate_exp: false,
sub: Some("Keats".to_string()),
..Default::default()
};
@ -313,6 +371,24 @@ mod tests {
let mut claims = Map::new();
claims.insert("sub".to_string(), to_value("Hacked").unwrap());
let validation = Validation {
validate_exp: false,
sub: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidSubject => (),
_ => assert!(false),
};
}
#[test]
fn sub_missing_fails() {
let claims = Map::new();
let validation = Validation {
validate_exp: false,
sub: Some("Keats".to_string()),
..Default::default()
};
@ -329,7 +405,10 @@ mod tests {
fn aud_string_ok() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
let mut validation = Validation {
validate_exp: false,
..Validation::default()
};
validation.set_audience(&"Everyone");
let res = validate(&claims, &validation);
assert!(res.is_ok());
@ -339,7 +418,10 @@ mod tests {
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::default();
let mut validation = Validation {
validate_exp: false,
..Validation::default()
};
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
assert!(res.is_ok());
@ -349,7 +431,10 @@ mod tests {
fn aud_type_mismatch_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
let mut validation = Validation {
validate_exp: false,
..Validation::default()
};
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
assert!(res.is_err());
@ -364,7 +449,27 @@ mod tests {
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::default();
let mut validation = Validation {
validate_exp: false,
..Validation::default()
};
validation.set_audience(&"None");
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidAudience => (),
_ => assert!(false),
};
}
#[test]
fn aud_missing_fails() {
let claims = Map::new();
let mut validation = Validation {
validate_exp: false,
..Validation::default()
};
validation.set_audience(&"None");
let res = validate(&claims, &validation);
assert!(res.is_err());

View File

@ -1,14 +1,16 @@
extern crate jsonwebtoken;
#[macro_use]
extern crate serde_derive;
extern crate chrono;
use jsonwebtoken::{encode, decode, decode_header, dangerous_unsafe_decode, Algorithm, Header, sign, verify, Validation};
use chrono::Utc;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String
company: String,
exp: i64,
}
#[test]
@ -29,7 +31,8 @@ fn verify_hs256() {
fn encode_with_custom_header() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string()
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let mut header = Header::default();
header.kid = Some("kid".to_string());
@ -43,7 +46,8 @@ fn encode_with_custom_header() {
fn round_trip_claim() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string()
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
let token_data = decode::<Claims>(&token, "secret".as_ref(), &Validation::default()).unwrap();
@ -53,8 +57,9 @@ fn round_trip_claim() {
#[test]
fn decode_token() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY";
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98";
let claims = decode::<Claims>(token, "secret".as_ref(), &Validation::default());
println!("{:?}", claims);
claims.unwrap();
}
@ -84,18 +89,11 @@ fn decode_token_wrong_algorithm() {
#[test]
fn decode_token_with_bytes_secret() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29tcGFueSI6Ikdvb2dvbCJ9.27QxgG96vpX4akKNpD1YdRGHE3_u2X35wR3EHA2eCrs";
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks";
let claims = decode::<Claims>(token, b"\x01\x02\x03", &Validation::default());
assert!(claims.is_ok());
}
#[test]
fn decode_token_with_shuffled_header_fields() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.SEIZ4Jg46VGhquuwPYDLY5qHF8AkQczF14aXM3a2c28";
let claims = decode::<Claims>(token, "secret".as_ref(), &Validation::default());
assert!(claims.is_ok());
}
#[test]
fn decode_header_only() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S";
@ -106,7 +104,7 @@ fn decode_header_only() {
#[test]
fn dangerous_unsafe_decode_token() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY";
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
@ -121,14 +119,36 @@ fn dangerous_unsafe_decode_token_missing_parts() {
#[test]
fn dangerous_unsafe_decode_token_invalid_signature() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong";
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
fn dangerous_unsafe_decode_token_wrong_algorithm() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY";
let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
// https://github.com/Keats/jsonwebtoken/issues/51
#[test]
fn does_validation_in_right_order() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
let v = Validation {
leeway: 5,
validate_exp: true,
iss: Some("iss no check".to_string()),
sub: Some("sub no check".to_string()),
..Validation::default()
};
let res = decode::<Claims>(&token, "secret".as_ref(), &v);
assert!(res.is_err());
println!("{:?}", res);
//assert!(res.is_ok());
}

View File

@ -1,14 +1,16 @@
extern crate jsonwebtoken;
#[macro_use]
extern crate serde_derive;
extern crate chrono;
use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify, Validation};
use chrono::Utc;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String
company: String,
exp: i64,
}
#[test]
@ -23,7 +25,8 @@ fn round_trip_sign_verification() {
fn round_trip_claim() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string()
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")).unwrap();
let token_data = decode::<Claims>(&token, include_bytes!("public_rsa_key.der"), &Validation::new(Algorithm::RS256)).unwrap();