use crate::error::jwt_error::JWTError;
use crate::parser::jwt_claims_parser::JWTClaimsParser;
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[cfg(not(any(feature = "fips", fips)))]
use jwt_compact::Token;
#[cfg(not(any(feature = "fips", fips)))]
use jwt_simple::prelude::{Audiences, UnixTimeStamp};
pub mod claim_names {
pub const AUD_CLAIM: &str = "aud";
pub const SUBJECT_CLAIM: &str = "sub";
pub const ISSUER_CLAIM: &str = "iss";
pub const JTI_CLAIM: &str = "jti";
pub const NONCE_CLAIM: &str = "nonce";
pub const EXPIRATION_CLAIM: &str = "exp";
pub const NOT_BEFORE_CLAIM: &str = "nbf";
pub const ISSUED_AT_CLAIM: &str = "iat";
}
use claim_names::*;
use pdk_script::IntoValue;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomClaims {
#[serde(flatten)]
pub inner: HashMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct JWTClaims {
#[serde(flatten)]
pub(crate) claims: HashMap<String, serde_json::Value>,
#[serde(flatten)]
pub(crate) headers: HashMap<String, serde_json::Value>,
}
impl JWTClaims {
pub fn new(
expiration: Option<DateTime<Utc>>,
not_before: Option<DateTime<Utc>>,
issued_at: Option<DateTime<Utc>>,
claims: HashMap<String, Value>,
headers: HashMap<String, Value>,
) -> Result<Self, JWTError> {
let mut claims = claims;
extract_datetime_value(EXPIRATION_CLAIM, &expiration, &mut claims);
extract_datetime_value(NOT_BEFORE_CLAIM, ¬_before, &mut claims);
extract_datetime_value(ISSUED_AT_CLAIM, &issued_at, &mut claims);
Ok(JWTClaims { claims, headers })
}
pub fn audience(&self) -> Option<Result<Vec<String>, JWTError>> {
self.claims.get(AUD_CLAIM).map(parse_audiences_to_list)
}
pub fn not_before(&self) -> Option<DateTime<Utc>> {
self.get_claim(NOT_BEFORE_CLAIM)
}
pub fn expiration(&self) -> Option<DateTime<Utc>> {
self.get_claim(EXPIRATION_CLAIM)
}
pub fn issued_at(&self) -> Option<DateTime<Utc>> {
self.get_claim(ISSUED_AT_CLAIM)
}
pub fn issuer(&self) -> Option<String> {
self.get_claim(ISSUER_CLAIM)
}
pub fn jti(&self) -> Option<String> {
self.get_claim(JTI_CLAIM)
}
pub fn nonce(&self) -> Option<String> {
self.get_claim(NONCE_CLAIM)
}
pub fn subject(&self) -> Option<String> {
self.get_claim(SUBJECT_CLAIM)
}
pub fn has_claim(&self, name: &str) -> bool {
self.claims.contains_key(name)
}
pub fn get_claim<T>(&self, name: &str) -> Option<T>
where
T: ValueRetrieval,
{
let value = self.claims.get(name);
match value {
Some(val) => T::retrieve(val.clone()),
None => None,
}
}
pub fn has_header(&self, name: &str) -> bool {
self.headers.contains_key(name)
}
pub fn get_header(&self, name: &str) -> Option<String> {
let value = self.headers.get(name);
match value {
Some(val) => val.as_str().map(|str| str.into()),
None => None,
}
}
pub fn to_serde(&self) -> Result<Value, JWTError> {
serde_json::to_value(self)
.map_err(|e| JWTError::TokenParseFailed(format!("Error serializing claims: {e}")))
}
pub fn get_claims(&self) -> pdk_script::Value {
self.claims.clone().into_value()
}
pub fn get_headers(&self) -> pdk_script::Value {
self.headers.clone().into_value()
}
}
fn parse_audiences_to_list(aud: &Value) -> Result<Vec<String>, JWTError> {
let correct_type = if aud.is_string() || aud.is_array() {
Ok(aud)
} else {
Err(JWTError::ClaimAudValidationFailed)
}?;
if correct_type.is_string() {
let aud_str = correct_type
.as_str()
.ok_or(JWTError::ClaimAudValidationFailed)?;
Ok(vec![String::from(aud_str)])
} else {
let aud_array = correct_type
.as_array()
.ok_or(JWTError::ClaimAudValidationFailed)?;
Ok(aud_array
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<String>>())
}
}
pub struct RawClaims {
pub expiration: Option<DateTime<Utc>>,
pub not_before: Option<DateTime<Utc>>,
pub issued_at: Option<DateTime<Utc>>,
pub custom: CustomClaims,
}
impl RawClaims {
pub fn from_json_slice(payload: &[u8]) -> Result<Self, JWTError> {
let mut inner: HashMap<String, Value> = serde_json::from_slice(payload)?;
let expiration = take_timestamp(&mut inner, EXPIRATION_CLAIM)?;
let not_before = take_timestamp(&mut inner, NOT_BEFORE_CLAIM)?;
let issued_at = take_timestamp(&mut inner, ISSUED_AT_CLAIM)?;
Ok(RawClaims {
expiration,
not_before,
issued_at,
custom: CustomClaims { inner },
})
}
}
fn take_timestamp(
inner: &mut HashMap<String, Value>,
key: &str,
) -> Result<Option<DateTime<Utc>>, JWTError> {
let value = match inner.remove(key) {
Some(value) => value,
None => return Ok(None),
};
let secs = value.as_f64().ok_or_else(|| {
JWTError::TokenParseFailed(format!("Claim '{key}' is not a numeric date"))
})?;
if !secs.is_finite() || secs < 0.0 {
return Err(JWTError::TokenParseFailed(format!(
"Claim '{key}' is not a valid NumericDate"
)));
}
Utc.timestamp_opt(secs as i64, 0)
.single()
.map(Some)
.ok_or_else(|| JWTError::TokenParseFailed(format!("Claim '{key}' is out of range")))
}
pub trait TryFromRawClaims {
fn try_from_raw_claims(claims: RawClaims, token: String) -> Result<JWTClaims, JWTError>;
}
impl TryFromRawClaims for JWTClaims {
fn try_from_raw_claims(claims: RawClaims, token: String) -> Result<JWTClaims, JWTError> {
let headers: HashMap<String, Value> = JWTClaimsParser::parse_headers(token)?;
JWTClaims::new(
claims.expiration,
claims.not_before,
claims.issued_at,
claims.custom.inner,
headers,
)
}
}
#[cfg(not(any(feature = "fips", fips)))]
pub trait TryFromTrustedJWTCompactClaims {
fn try_from_trusted_jwt_compact_claims(
claims: Token<CustomClaims>,
token: String,
) -> Result<JWTClaims, JWTError>;
}
#[cfg(not(any(feature = "fips", fips)))]
impl TryFromTrustedJWTCompactClaims for JWTClaims {
fn try_from_trusted_jwt_compact_claims(
trusted_token: Token<CustomClaims>,
token_str: String,
) -> Result<Self, JWTError> {
let claims = trusted_token.claims().to_owned();
let raw = RawClaims {
expiration: claims.expiration,
not_before: claims.not_before,
issued_at: claims.issued_at,
custom: claims.custom,
};
JWTClaims::try_from_raw_claims(raw, token_str)
}
}
#[cfg(not(any(feature = "fips", fips)))]
pub trait TryFromTrustedJWTSimpleClaims {
fn try_from_trusted_jwt_simple_claims(
claims: jwt_simple::claims::JWTClaims<CustomClaims>,
token: String,
) -> Result<JWTClaims, JWTError>;
}
#[cfg(not(any(feature = "fips", fips)))]
impl TryFromTrustedJWTSimpleClaims for JWTClaims {
fn try_from_trusted_jwt_simple_claims(
claims: jwt_simple::claims::JWTClaims<CustomClaims>,
token: String,
) -> Result<Self, JWTError> {
let mut custom_claims = claims.custom.inner;
extract_value(ISSUER_CLAIM, &claims.issuer, &mut custom_claims);
extract_value(SUBJECT_CLAIM, &claims.subject, &mut custom_claims);
extract_value(JTI_CLAIM, &claims.jwt_id, &mut custom_claims);
extract_value(NONCE_CLAIM, &claims.nonce, &mut custom_claims);
parse_custom_claim_audiences_to_list(claims.audiences, &mut custom_claims);
extract_numeric_date(EXPIRATION_CLAIM, &claims.expires_at, &mut custom_claims);
extract_numeric_date(NOT_BEFORE_CLAIM, &claims.invalid_before, &mut custom_claims);
extract_numeric_date(ISSUED_AT_CLAIM, &claims.issued_at, &mut custom_claims);
let headers: HashMap<String, Value> = JWTClaimsParser::parse_headers(token)?;
JWTClaims::new(None, None, None, custom_claims, headers)
}
}
#[cfg(not(any(feature = "fips", fips)))]
fn parse_custom_claim_audiences_to_list(
aud: Option<Audiences>,
custom_claims: &mut HashMap<String, Value>,
) {
if let Some(audiences) = aud {
match audiences {
Audiences::AsSet(set) => {
if let Ok(value_set) = serde_json::to_value(set) {
custom_claims.insert(AUD_CLAIM.to_string(), value_set);
}
}
Audiences::AsString(str) => {
custom_claims.insert(AUD_CLAIM.to_string(), Value::String(str));
}
}
}
}
pub trait ValueRetrieval {
fn retrieve(claim_value: serde_json::Value) -> Option<Self>
where
Self: Sized;
}
impl ValueRetrieval for Value {
fn retrieve(claim_value: serde_json::Value) -> Option<Self> {
Some(claim_value)
}
}
impl ValueRetrieval for String {
fn retrieve(claim_value: serde_json::Value) -> Option<Self> {
Some(claim_value.as_str().unwrap().into())
}
}
impl ValueRetrieval for f64 {
fn retrieve(claim_value: serde_json::Value) -> Option<Self> {
claim_value.as_f64()
}
}
impl ValueRetrieval for Vec<String> {
fn retrieve(claim_value: serde_json::Value) -> Option<Self> {
claim_value.as_array().map(|val| {
val.iter()
.map(|item| item.as_str().unwrap().into())
.collect()
})
}
}
impl ValueRetrieval for DateTime<Utc> {
fn retrieve(claim_value: serde_json::Value) -> Option<Self> {
claim_value
.as_i64()
.and_then(|val| Utc.timestamp_opt(val, 0).earliest())
}
}
fn extract_datetime_value(
key: &str,
datetime: &Option<DateTime<Utc>>,
custom_claims: &mut HashMap<String, Value>,
) {
if let Some(datetime) = datetime {
custom_claims.insert(key.to_string(), Value::from(datetime.timestamp()));
}
}
#[cfg(not(any(feature = "fips", fips)))]
fn extract_value(
key: &str,
claim_value: &Option<String>,
custom_claims: &mut HashMap<String, Value>,
) {
if let Some(claim) = claim_value {
let value = Value::String(claim.to_string());
custom_claims.insert(key.to_string(), value);
}
}
#[cfg(not(any(feature = "fips", fips)))]
fn extract_numeric_date(
key: &str,
claim_value: &Option<UnixTimeStamp>,
custom_claims: &mut HashMap<String, Value>,
) {
if let Some(claim) = claim_value {
let value = Value::from(claim.as_secs());
custom_claims.insert(key.to_string(), value);
}
}
#[cfg(test)]
mod tests {
use crate::api::JWTClaimsParser;
use pdk_script::IntoValue;
use serde_json::json;
#[cfg(not(any(feature = "fips", fips)))]
fn valid_token() -> String {
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JlZGFjdGVkLmFuei5jb20iLCJhdWQiOiJyZWRhY3RlZC5hbnouY29tL2VtYWlsQWRkcmVzcy1yZWRhY3RlZEBhbnouY29tIiwic3ViIjoicmVkYWN0ZWQuYW56LmNvbS9lbWFpbEFkZHJlc3MtcmVkYWN0ZWRAYW56LmNvbSIsImV4cCI6MTY5Mzk4ODY1Ni41OTcsInNjb3BlcyI6WyJyZWRhY3RlZDEiLCJyZWRhY3RlZCJdLCJhbXIiOlsicmVkYWN0ZWQiXSwiYWNyIjoicmVkYWN0ZWQifQ.EswBSyD9976PVC6o4tWwsT5rGTf2RJcvL7hdpcTwXUo"
.to_string()
}
fn no_exp_token() -> String {
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JlZGFjdGVkLmFuei5jb20iLCJhdWQiOiJyZWRhY3RlZC5hbnouY29tL2VtYWlsQWRkcmVzcy1yZWRhY3RlZEBhbnouY29tIiwic3ViIjoicmVkYWN0ZWQuYW56LmNvbS9lbWFpbEFkZHJlc3MtcmVkYWN0ZWRAYW56LmNvbSIsInNjb3BlcyI6WyJyZWRhY3RlZDEiLCJyZWRhY3RlZCJdLCJhbXIiOlsicmVkYWN0ZWQiXSwiYWNyIjoicmVkYWN0ZWQifQ.vxNPrXPIG8VuGb9Bf2MjLFXPRX9EmN97WZtM-jzQniE"
.to_string()
}
mod tests_accessors {
use crate::{error::jwt_error::JWTError, model::claims::JWTClaims};
use chrono::{DateTime, Utc};
use serde_json::Value;
use std::collections::HashMap;
impl JWTClaims {
pub fn empty() -> Result<Self, JWTError> {
JWTClaims::new(None, None, None, HashMap::default(), HashMap::default())
}
pub fn just_expiration(expiration: DateTime<Utc>) -> Result<Self, JWTError> {
JWTClaims::new(
Some(expiration),
None,
None,
HashMap::default(),
HashMap::default(),
)
}
pub fn just_not_before(not_before: DateTime<Utc>) -> Result<Self, JWTError> {
JWTClaims::new(
None,
Some(not_before),
None,
HashMap::default(),
HashMap::default(),
)
}
pub fn from_custom_claims(
custom_claims: HashMap<String, Value>,
) -> Result<Self, JWTError> {
JWTClaims::new(None, None, None, custom_claims, HashMap::default())
}
pub fn custom_claims_count(&self) -> usize {
self.claims.len()
}
}
}
#[test]
fn basic_methods() {
let claims = JWTClaimsParser::parse(no_exp_token()).unwrap();
assert!(claims.has_header("alg"));
assert!(!claims.has_claim("alg"));
assert!(claims.has_claim("iss"));
assert!(!claims.has_header("iss"));
assert_eq!(
claims.get_headers(),
json!({
"alg": "HS256",
"typ": "JWT"
})
.into_value()
);
assert_eq!(
claims.get_claims(),
json!(
{
"iss": "https://redacted.anz.com",
"aud": "redacted.anz.com/emailAddress-redacted@anz.com",
"sub": "redacted.anz.com/emailAddress-redacted@anz.com",
"scopes": [
"redacted1",
"redacted"
],
"amr": [
"redacted"
],
"acr": "redacted"
}
)
.into_value()
);
}
mod audience_claim_extraction {
use crate::model::claims::{claim_names::AUD_CLAIM, JWTClaims};
use serde_json::Value::*;
use serde_json::{Map, Number, Value};
use std::collections::HashMap;
use std::string::String;
#[test]
fn succeeds_on_string_type() {
let token_claims = JWTClaims::from_custom_claims(HashMap::from([(
AUD_CLAIM.to_string(),
String(String::from("an_audience")),
)]))
.unwrap();
assert!(token_claims.audience().is_some());
assert!(token_claims.audience().unwrap().is_ok());
assert_eq!(
token_claims.audience().unwrap().unwrap(),
vec![String::from("an_audience")]
);
}
#[test]
fn succeeds_on_list_type() {
let token_claims = JWTClaims::from_custom_claims(HashMap::from([(
AUD_CLAIM.to_string(),
Array(vec![
String(String::from("an_audience1")),
String(String::from("an_audience2")),
]),
)]))
.unwrap();
assert!(token_claims.audience().is_some());
assert!(token_claims.audience().unwrap().is_ok());
assert_eq!(
token_claims.audience().unwrap().unwrap(),
vec![String::from("an_audience1"), String::from("an_audience2")]
);
}
#[test]
fn fails_on_bool_type() {
let token_claims =
JWTClaims::from_custom_claims(HashMap::from([(AUD_CLAIM.to_string(), Bool(true))]))
.unwrap();
assert!(token_claims.audience().is_some());
assert!(token_claims.audience().unwrap().is_err());
}
#[test]
fn fails_on_number_type() {
let token_claims = JWTClaims::from_custom_claims(HashMap::from([(
AUD_CLAIM.to_string(),
Number(Number::from(1u64)),
)]))
.unwrap();
assert!(token_claims.audience().is_some());
assert!(token_claims.audience().unwrap().is_err());
}
#[test]
fn fails_on_null_type() {
let token_claims =
JWTClaims::from_custom_claims(HashMap::from([(AUD_CLAIM.to_string(), Null)]))
.unwrap();
assert!(token_claims.audience().is_some());
assert!(token_claims.audience().unwrap().is_err());
}
#[test]
fn fails_on_object_type() {
let token_claims = JWTClaims::from_custom_claims(HashMap::from([(
AUD_CLAIM.to_string(),
Object(Map::<String, Value>::new()),
)]))
.unwrap();
assert!(token_claims.audience().is_some());
assert!(token_claims.audience().unwrap().is_err());
}
}
#[cfg(not(any(feature = "fips", fips)))]
mod jwt_simple_custom_claims_parsing {
use super::super::*;
use chrono::Duration;
#[test]
fn huge_exp_defaults_to_max_timestamp() {
let claims: jwt_simple::claims::JWTClaims<CustomClaims> =
serde_json::from_str(r#"{"exp":99999999999999999999999999999999}"#).unwrap();
let time = UnixTimeStamp::new(u64::MAX, 0);
assert_eq!(claims.expires_at, Some(time));
let jwt_claims: JWTClaims =
JWTClaims::try_from_trusted_jwt_simple_claims(claims, super::valid_token())
.unwrap();
assert_eq!(
jwt_claims.expiration().unwrap().timestamp(),
time.as_secs() as i64
);
assert_eq!(
jwt_claims.expiration().unwrap().timestamp(),
u32::MAX as i64
);
}
#[test]
fn parse_claims_and_exp() {
let claims: jwt_simple::claims::JWTClaims<CustomClaims> = serde_json::from_str(r#"{"exp":1617757825, "jti": "key1", "sub": "anSubject", "iss": "anIss", "nonce": "anNonce"}"#).unwrap();
let expiration = Duration::try_seconds(1617757825).unwrap();
let jwt_claims: JWTClaims =
JWTClaims::try_from_trusted_jwt_simple_claims(claims, super::valid_token())
.unwrap();
assert_eq!(
jwt_claims
.get_claim::<DateTime<Utc>>("exp")
.unwrap()
.timestamp_millis(),
expiration.num_milliseconds()
);
assert_eq!(jwt_claims.get_claim::<String>("jti").unwrap(), "key1");
assert_eq!(jwt_claims.get_claim::<String>("sub").unwrap(), "anSubject");
assert_eq!(jwt_claims.get_claim::<String>("iss").unwrap(), "anIss");
assert_eq!(jwt_claims.get_claim::<String>("nonce").unwrap(), "anNonce");
assert!(jwt_claims.audience().is_none());
assert_eq!(
jwt_claims.expiration().unwrap().timestamp_millis(),
expiration.num_milliseconds()
);
assert_eq!(jwt_claims.jti().unwrap(), "key1");
assert_eq!(jwt_claims.subject().unwrap(), "anSubject");
assert_eq!(jwt_claims.issuer().unwrap(), "anIss");
assert_eq!(jwt_claims.nonce().unwrap(), "anNonce");
}
#[test]
fn parse_custom_and_audiences_with_vec_claims() {
let claims: jwt_simple::claims::JWTClaims<CustomClaims> = serde_json::from_str(r#"{"exp":1617757827, "otherClaim": "anCustomclaim", "aud": ["audience1", "audience2"]}"#).unwrap();
let jwt_claims =
JWTClaims::try_from_trusted_jwt_simple_claims(claims, super::valid_token())
.unwrap();
assert_eq!(
jwt_claims.get_claim::<String>("otherClaim").unwrap(),
"anCustomclaim"
);
assert!(jwt_claims.audience().is_some());
let audiences = jwt_claims.audience().unwrap();
assert!(audiences.is_ok());
let audiences_vec = audiences.unwrap();
assert_eq!(audiences_vec.len(), 2);
assert!(audiences_vec.contains(&String::from("audience1")));
assert!(audiences_vec.contains(&String::from("audience2")));
}
#[test]
fn parse_audiences_with_string_claims() {
let claims: jwt_simple::claims::JWTClaims<CustomClaims> =
serde_json::from_str(r#"{"exp":1617757827, "aud": "audience1"}"#).unwrap();
let jwt_claims =
JWTClaims::try_from_trusted_jwt_simple_claims(claims, super::valid_token())
.unwrap();
assert!(jwt_claims.audience().is_some());
let audiences = jwt_claims.audience().unwrap();
assert!(audiences.is_ok());
let audiences_vec = audiences.unwrap();
assert_eq!(audiences_vec.len(), 1);
assert!(audiences_vec.contains(&String::from("audience1")));
}
#[test]
fn parse_other_claims() {
let claims: jwt_simple::claims::JWTClaims<CustomClaims> =
serde_json::from_str(r#"{"exp":1617757827, "iat":1617757827, "nbf": 1617757827}"#)
.unwrap();
let duration = Duration::try_seconds(1617757827).unwrap();
let jwt_claims =
JWTClaims::try_from_trusted_jwt_simple_claims(claims, super::valid_token())
.unwrap();
assert_eq!(
jwt_claims
.get_claim::<DateTime<Utc>>("nbf")
.unwrap()
.timestamp_millis(),
duration.num_milliseconds()
);
assert_eq!(
jwt_claims
.get_claim::<DateTime<Utc>>("iat")
.unwrap()
.timestamp_millis(),
duration.num_milliseconds()
);
assert_eq!(
jwt_claims
.get_claim::<DateTime<Utc>>("exp")
.unwrap()
.timestamp_millis(),
duration.num_milliseconds()
);
assert_eq!(
jwt_claims.not_before().unwrap().timestamp_millis(),
duration.num_milliseconds()
);
assert_eq!(
jwt_claims.issued_at().unwrap().timestamp_millis(),
duration.num_milliseconds()
);
assert_eq!(
jwt_claims.expiration().unwrap().timestamp_millis(),
duration.num_milliseconds()
);
assert_eq!(jwt_claims.get_header("alg").unwrap(), "HS256",);
assert_eq!(jwt_claims.get_header("typ").unwrap(), "JWT",);
}
}
}