about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2018-09-04T10·33+0200
committerVincent Ambo <mail@tazj.in>2018-09-04T10·45+0200
commitdd527ecdf1f8c979a06ade426c22d37c2a4a06ea (patch)
treefd813ab4fb64772ea0504d4adfd75127ac6cd82f
parentae409995ca045c77e759a68512125e71b0f14d90 (diff)
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
-rw-r--r--Cargo.toml4
-rw-r--r--src/lib.rs112
2 files changed, 109 insertions, 7 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 2b7bb7b82dd3..15eccc357a4e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,8 +4,8 @@ version = "0.1.0"
 authors = ["Vincent Ambo <vincent@aprila.no>"]
 
 [dependencies]
+base64 = "0.9"
 openssl = "0.10"
 serde = "1.0"
-serde_json = "1.0"
 serde_derive = "1.0"
-base64 = "0.9"
+serde_json = "1.0"
diff --git a/src/lib.rs b/src/lib.rs
index 77c91370a622..a61e793e2c17 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -32,8 +32,7 @@
 //!
 //! // Several types of built-in validations are provided:
 //! let validations = vec![
-//!   Validation::Issuer("some-issuer".into()),
-//!   Validation::Audience("some-audience".into()),
+//!   Validation::Issuer("auth.test.aprila.no".into()),
 //!   Validation::SubjectPresent,
 //! ];
 //!
@@ -66,6 +65,7 @@ use openssl::rsa::Rsa;
 use openssl::sign::Verifier;
 use serde::de::DeserializeOwned;
 use serde_json::Value;
+use std::time::{UNIX_EPOCH, Duration, SystemTime};
 
 #[cfg(test)]
 mod tests;
@@ -152,6 +152,10 @@ pub enum Validation {
 
     /// Validate that a subject value is present.
     SubjectPresent,
+
+    /// Validate that the expiry time of the token ("exp"-claim) has
+    /// not yet been reached.
+    NotExpired,
 }
 
 /// Possible results of a token validation.
@@ -174,9 +178,9 @@ pub enum ValidationError {
     /// JSON decoding into a provided type failed.
     JSON(serde_json::Error),
 
-    /// One or more claim validations failed.
-    // TODO: Provide reasons?
-    InvalidClaims,
+    /// One or more claim validations failed. This variant contains
+    /// human-readable validation errors.
+    InvalidClaims(Vec<&'static str>),
 }
 
 type JWTResult<T> = Result<T, ValidationError>;
@@ -243,6 +247,10 @@ pub fn validate(token: String,
         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 claims = deserialize_part(parts[1])?;
     let valid_jwt = ValidJWT { headers, claims };
@@ -315,3 +323,97 @@ fn validate_jwt_signature(jwt: &JWT, key: Rsa<Public>) -> JWTResult<()> {
         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))
+    }
+}