feat: Implement claim validation

Implements initial validations of token claims. The included
validations are:

* validation of token issuer
* validation of token audience
* validation that a subject is set
* validation that a token is not expired
This commit is contained in:
Vincent Ambo 2018-09-04 12:33:30 +02:00
parent ae409995ca
commit dd527ecdf1
2 changed files with 109 additions and 7 deletions

View file

@ -4,8 +4,8 @@ version = "0.1.0"
authors = ["Vincent Ambo <vincent@aprila.no>"] authors = ["Vincent Ambo <vincent@aprila.no>"]
[dependencies] [dependencies]
base64 = "0.9"
openssl = "0.10" openssl = "0.10"
serde = "1.0" serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
base64 = "0.9" serde_json = "1.0"

View file

@ -32,8 +32,7 @@
//! //!
//! // Several types of built-in validations are provided: //! // Several types of built-in validations are provided:
//! let validations = vec![ //! let validations = vec![
//! Validation::Issuer("some-issuer".into()), //! Validation::Issuer("auth.test.aprila.no".into()),
//! Validation::Audience("some-audience".into()),
//! Validation::SubjectPresent, //! Validation::SubjectPresent,
//! ]; //! ];
//! //!
@ -66,6 +65,7 @@ use openssl::rsa::Rsa;
use openssl::sign::Verifier; use openssl::sign::Verifier;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::Value; use serde_json::Value;
use std::time::{UNIX_EPOCH, Duration, SystemTime};
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -152,6 +152,10 @@ pub enum Validation {
/// Validate that a subject value is present. /// Validate that a subject value is present.
SubjectPresent, SubjectPresent,
/// Validate that the expiry time of the token ("exp"-claim) has
/// not yet been reached.
NotExpired,
} }
/// Possible results of a token validation. /// Possible results of a token validation.
@ -174,9 +178,9 @@ pub enum ValidationError {
/// JSON decoding into a provided type failed. /// JSON decoding into a provided type failed.
JSON(serde_json::Error), JSON(serde_json::Error),
/// One or more claim validations failed. /// One or more claim validations failed. This variant contains
// TODO: Provide reasons? /// human-readable validation errors.
InvalidClaims, InvalidClaims(Vec<&'static str>),
} }
type JWTResult<T> = Result<T, ValidationError>; type JWTResult<T> = Result<T, ValidationError>;
@ -243,6 +247,10 @@ pub fn validate(token: String,
return Err(ValidationError::MalformedJWT) return Err(ValidationError::MalformedJWT)
} }
// Perform claim validations before constructing the valid token:
let partial_claims = deserialize_part(parts[1])?;
validate_claims(partial_claims, validations)?;
let headers = deserialize_part(parts[0])?; let headers = deserialize_part(parts[0])?;
let claims = deserialize_part(parts[1])?; let claims = deserialize_part(parts[1])?;
let valid_jwt = ValidJWT { headers, claims }; let valid_jwt = ValidJWT { headers, claims };
@ -315,3 +323,97 @@ fn validate_jwt_signature(jwt: &JWT, key: Rsa<Public>) -> JWTResult<()> {
false => Err(ValidationError::InvalidSignature), false => Err(ValidationError::InvalidSignature),
} }
} }
/// Internal helper struct for claims that are relevant for claim
/// validations.
#[derive(Deserialize)]
struct PartialClaims {
aud: Option<String>,
iss: Option<String>,
sub: Option<String>,
exp: Option<u64>,
}
/// Apply a single validation to the claim set of a token.
fn apply_validation(claims: &PartialClaims,
validation: Validation) -> Result<(), &'static str> {
match validation {
// Validate that an 'iss' claim is present and matches the
// supplied value.
Validation::Issuer(iss) => {
match claims.iss {
None => Err("'iss' claim is missing"),
Some(ref claim) => if *claim == iss {
Ok(())
} else {
Err("'iss' claim does not match")
}
}
},
// Validate that an 'aud' claim is present and matches the
// supplied value.
Validation::Audience(aud) => {
match claims.aud {
None => Err("'aud' claim is missing"),
Some(ref claim) => if *claim == aud {
Ok(())
} else {
Err("'aud' claim does not match")
}
}
},
Validation::SubjectPresent => match claims.sub {
Some(_) => Ok(()),
None => Err("'sub' claim is missing"),
},
Validation::NotExpired => match claims.exp {
None => Err("'exp' claim is missing"),
Some(exp) => {
// Determine the current timestamp in seconds since
// the UNIX epoch.
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
// this is an unrecoverable, critical error. There
// aren't many ways this can occur, other than
// system time being set into the far future or
// this library being used in some sort of future
// museum.
.expect("system time is likely incorrect");
// Convert the expiry time (which is also in epoch
// seconds) to a duration.
let exp_duration = Duration::from_secs(exp);
// The token has not expired if the expiry duration is
// larger than (i.e. in the future from) the current
// time.
if exp_duration > now {
Ok(())
} else {
Err("token has expired")
}
}
},
}
}
/// Apply all requested validations to a partial claim set.
fn validate_claims(claims: PartialClaims,
validations: Vec<Validation>) -> JWTResult<()> {
let validation_errors: Vec<_> = validations.into_iter()
.map(|v| apply_validation(&claims, v))
.filter_map(|result| match result {
Ok(_) => None,
Err(err) => Some(err),
})
.collect();
if validation_errors.is_empty() {
Ok(())
} else {
Err(ValidationError::InvalidClaims(validation_errors))
}
}