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:
parent
ae409995ca
commit
dd527ecdf1
2 changed files with 109 additions and 7 deletions
|
@ -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"
|
||||||
|
|
112
src/lib.rs
112
src/lib.rs
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue