diff --git a/auth-helper/CHANGELOG.md b/auth-helper/CHANGELOG.md index 0398945..a5c1094 100644 --- a/auth-helper/CHANGELOG.md +++ b/auth-helper/CHANGELOG.md @@ -19,6 +19,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 0.2.0 - 2022-01-25 + +### Added + + - Capability to create tokens that do not expire; + - `jwt::Error` type; + - `ClaimsBuilder` for optional claims; + +### Changed + + - `JsonWebToken` interface changes; + ## 0.1.0 - 2022-01-20 ### Added diff --git a/auth-helper/Cargo.toml b/auth-helper/Cargo.toml index 8ec01d6..1a69f10 100644 --- a/auth-helper/Cargo.toml +++ b/auth-helper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "auth-helper" -version = "0.1.0" +version = "0.2.0" authors = [ "IOTA Stiftung" ] edition = "2021" description = "Authorization tools" @@ -15,3 +15,4 @@ jsonwebtoken = { version = "7.2.0", default-features = false } rand = { version = "0.8.4", default-features = false, features = [ "std" ] } rust-argon2 = { version = "0.8.3", default-features = false } serde = { version = "1.0.30", default-features = false, features = [ "std", "derive" ] } +thiserror = { version = "1.0.30", default-features = false } diff --git a/auth-helper/src/jwt.rs b/auth-helper/src/jwt.rs index fda6a76..e9ce8af 100644 --- a/auth-helper/src/jwt.rs +++ b/auth-helper/src/jwt.rs @@ -3,15 +3,24 @@ //! A module that provides JSON Web Token utilities. +pub use jsonwebtoken::TokenData; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -pub use jsonwebtoken::{ - errors::{Error, ErrorKind}, - TokenData, -}; use serde::{Deserialize, Serialize}; +use thiserror::Error; use std::time::{SystemTime, UNIX_EPOCH}; +/// JsonWebToken error. +#[derive(Error, Debug)] +pub enum Error { + /// Provided an invalid expiry date. + #[error("invalid expiry time {expiry} from issue time {issued_at}")] + InvalidExpiry { issued_at: u64, expiry: u64 }, + /// An error occured in the [`jsonwebtoken`] crate. + #[error(transparent)] + Jwt(#[from] jsonwebtoken::errors::Error), +} + /// Represents registered JSON Web Token Claims. /// #[derive(Debug, Serialize, Deserialize)] @@ -35,7 +44,8 @@ pub struct Claims { /// the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" /// claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock /// skew. - exp: u64, + #[serde(skip_serializing_if = "Option::is_none")] + exp: Option, /// Not Before. /// Identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the "nbf" claim /// requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" @@ -49,12 +59,12 @@ pub struct Claims { impl Claims { /// Creates a new set of claims. - pub fn new(iss: String, sub: String, aud: String, exp: u64, nbf: u64) -> Self { + fn new(iss: String, sub: String, aud: String, nbf: u64) -> Self { Self { iss, sub, aud, - exp, + exp: None, nbf, iat: SystemTime::now() .duration_since(UNIX_EPOCH) @@ -62,6 +72,84 @@ impl Claims { .as_secs() as u64, } } + + /// Returns the issuer of the JWT. + pub fn issuer(&self) -> &str { + &self.iss + } + + /// Returns the subject of the JWT. + pub fn subject(&self) -> &str { + &self.sub + } + + /// Returns the audience of the JWT. + pub fn audience(&self) -> &str { + &self.aud + } + + /// Returns the expiration time of the JWT, if it has been specified. + pub fn expiry(&self) -> Option { + self.exp + } + + /// Returns the "nbf" field of the JWT. + pub fn not_before(&self) -> u64 { + self.nbf + } + + /// Returns the issue timestamp of the JWT. + pub fn issued_at(&self) -> u64 { + self.iat + } +} + +/// Builder for the [`Claims`] structure. +pub struct ClaimsBuilder { + iss: String, + sub: String, + aud: String, + exp: Option, +} + +impl ClaimsBuilder { + /// Creates a new [`ClaimsBuilder`] with the given mandatory parameters. + pub fn new(iss: String, sub: String, aud: String) -> Self { + Self { + iss, + sub, + aud, + exp: None, + } + } + + /// Specifies that this token will expire, and provides an expiry time (offset from issue time). + #[must_use] + pub fn with_expiry(mut self, exp: u64) -> Self { + self.exp = Some(exp); + self + } + + /// Builds and returns a [`Claims`] structure using the given builder options. + pub fn build(self) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Clock may have gone backwards") + .as_secs() as u64; + + let mut claims = Claims::new(self.iss, self.sub, self.aud, now); + + if let Some(exp) = self.exp { + let expiry = now.checked_add(exp).ok_or(Error::InvalidExpiry { + issued_at: now, + expiry: exp, + })?; + + claims.exp = Some(expiry); + } + + Ok(claims) + } } /// Represents a JSON Web Token. @@ -83,18 +171,7 @@ impl std::fmt::Display for JsonWebToken { impl JsonWebToken { /// Creates a new JSON Web Token. - pub fn new( - issuer: String, - subject: String, - audience: String, - session_timeout: u64, - secret: &[u8], - ) -> Result { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Clock may have gone backwards") - .as_secs() as u64; - let claims = Claims::new(issuer, subject, audience, now + session_timeout, now); + pub fn new(claims: Claims, secret: &[u8]) -> Result { let token = encode( &Header::default(), &claims, @@ -110,15 +187,21 @@ impl JsonWebToken { issuer: String, subject: String, audience: String, + expires: bool, secret: &[u8], ) -> Result, Error> { let mut validation = Validation { iss: Some(issuer), sub: Some(subject), + validate_exp: expires, ..Default::default() }; validation.set_audience(&[audience]); - decode::(&self.0, &DecodingKey::from_secret(secret), &validation) + Ok(decode::( + &self.0, + &DecodingKey::from_secret(secret), + &validation, + )?) } } diff --git a/auth-helper/tests/jwt.rs b/auth-helper/tests/jwt.rs index 213fdb7..4cff6b9 100644 --- a/auth-helper/tests/jwt.rs +++ b/auth-helper/tests/jwt.rs @@ -5,37 +5,43 @@ use auth_helper::jwt; #[test] fn jwt_valid() { - let jwt = jwt::JsonWebToken::new( + let claims = jwt::ClaimsBuilder::new( String::from("issuer"), String::from("subject"), String::from("audience"), - 1000, - b"secret", ) + .with_expiry(1000) + .build() .unwrap(); + let jwt = jwt::JsonWebToken::new(claims, b"secret").unwrap(); + assert!(jwt .validate( String::from("issuer"), String::from("subject"), String::from("audience"), - b"secret" + false, + b"secret", ) .is_ok()); } #[test] fn jwt_to_str_from_str_valid() { + let claims = jwt::ClaimsBuilder::new( + String::from("issuer"), + String::from("subject"), + String::from("audience"), + ) + .with_expiry(1000) + .build() + .unwrap(); + let jwt = jwt::JsonWebToken::from( - jwt::JsonWebToken::new( - String::from("issuer"), - String::from("subject"), - String::from("audience"), - 1000, - b"secret", - ) - .unwrap() - .to_string(), + jwt::JsonWebToken::new(claims, b"secret") + .unwrap() + .to_string(), ); assert!(jwt @@ -43,106 +49,121 @@ fn jwt_to_str_from_str_valid() { String::from("issuer"), String::from("subject"), String::from("audience"), - b"secret" + false, + b"secret", ) .is_ok()); } #[test] fn jwt_invalid_issuer() { - let jwt = jwt::JsonWebToken::new( + let claims = jwt::ClaimsBuilder::new( String::from("issuer"), String::from("subject"), String::from("audience"), - 1000, - b"secret", ) + .with_expiry(1000) + .build() .unwrap(); + let jwt = jwt::JsonWebToken::new(claims, b"secret").unwrap(); + assert!(jwt .validate( String::from("Issuer"), String::from("subject"), String::from("audience"), - b"secret" + false, + b"secret", ) .is_err()); } #[test] fn jwt_invalid_subject() { - let jwt = jwt::JsonWebToken::new( + let claims = jwt::ClaimsBuilder::new( String::from("issuer"), String::from("subject"), String::from("audience"), - 1000, - b"secret", ) + .with_expiry(1000) + .build() .unwrap(); + let jwt = jwt::JsonWebToken::new(claims, b"secret").unwrap(); + assert!(jwt .validate( String::from("issuer"), String::from("Subject"), String::from("audience"), - b"secret" + false, + b"secret", ) .is_err()); } #[test] fn jwt_invalid_audience() { - let jwt = jwt::JsonWebToken::new( + let claims = jwt::ClaimsBuilder::new( String::from("issuer"), String::from("subject"), String::from("audience"), - 1000, - b"secret", ) + .with_expiry(1000) + .build() .unwrap(); + let jwt = jwt::JsonWebToken::new(claims, b"secret").unwrap(); + assert!(jwt .validate( String::from("issuer"), String::from("subject"), String::from("Audience"), - b"secret" + false, + b"secret", ) .is_err()); } #[test] fn jwt_invalid_secret() { - let jwt = jwt::JsonWebToken::new( + let claims = jwt::ClaimsBuilder::new( String::from("issuer"), String::from("subject"), String::from("audience"), - 1000, - b"secret", ) + .with_expiry(1000) + .build() .unwrap(); + let jwt = jwt::JsonWebToken::new(claims, b"secret").unwrap(); + assert!(jwt .validate( String::from("issuer"), String::from("subject"), String::from("audience"), - b"Secret" + false, + b"Secret", ) .is_err()); } #[test] fn jwt_invalid_expired() { - let jwt = jwt::JsonWebToken::new( + let claims = jwt::ClaimsBuilder::new( String::from("issuer"), String::from("subject"), String::from("audience"), - 0, - b"secret", ) + .with_expiry(0) + .build() .unwrap(); + let jwt = jwt::JsonWebToken::new(claims, b"secret").unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); assert!(jwt @@ -150,7 +171,8 @@ fn jwt_invalid_expired() { String::from("issuer"), String::from("subject"), String::from("audience"), - b"secret" + true, + b"secret", ) .is_err()); }