use std::borrow::Cow;
use std::collections::HashSet;
use crate::common::issuer_utils::IssClaim;
use crate::common::policy_store::{TokenEntityMetadata, TrustedIssuer};
use crate::jwt::decode::{DecodeJwtError, DecodedJwt};
use crate::jwt::key_service::DecodingKeyInfo;
use crate::jwt::validation::TrustedIssuerError;
use crate::jwt::{
Arc, JwtStatus, JwtStatusError, OwnedValidatorInfo, StatusListCache, TokenKind, ValidatorInfo,
};
use jsonwebtoken::errors::{ErrorKind, new_error};
use jsonwebtoken::{self as jwt, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, PartialEq, Deserialize, Clone)]
pub(crate) struct ValidatedJwt {
#[serde(flatten)]
pub claims: Value,
#[serde(skip)]
pub trusted_iss: Option<Arc<TrustedIssuer>>,
}
impl ValidatedJwt {
pub(crate) fn get_ref_status(&self) -> Result<Option<RefJwtStatusList>, serde_json::Error> {
let Some(status) = self.claims.get("status") else {
return Ok(None);
};
let status_list = serde_json::from_value::<RefJwtStatusListClaim>(status.clone())?;
Ok(Some(status_list.status_list))
}
}
#[derive(Debug, Deserialize, PartialEq)]
struct RefJwtStatusListClaim {
status_list: RefJwtStatusList,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub(crate) struct RefJwtStatusList {
pub idx: usize,
pub uri: String,
#[serde(default)]
pub ttl: Option<u64>,
}
#[derive(Debug, Clone)]
pub(crate) struct JwtValidator {
pub(crate) validation: Validation,
expected_iss: Option<IssClaim>,
required_claims: HashSet<String>,
validate_signature: bool,
validate_status_list: bool,
status_list_cache: StatusListCache,
}
impl JwtValidator {
pub(super) fn new_input_tkn_validator<'a>(
iss: Option<&'a IssClaim>,
tkn_name: &'a str,
token_metadata: &TokenEntityMetadata,
algorithm: Algorithm,
status_lists: StatusListCache,
validate_signature: bool,
validate_status_list: bool,
) -> (Self, ValidatorInfo<'a>) {
let token_kind = TokenKind::AuthzRequestInput(tkn_name);
let mut validation = Validation::new(algorithm);
validation.validate_exp = true;
validation.validate_nbf = true;
validation.required_spec_claims.clear();
validation.validate_aud = false;
let required_claims = token_metadata.required_claims.iter().cloned().collect();
let key = ValidatorInfo {
iss,
token_kind,
algorithm,
};
let validator = JwtValidator {
validation,
expected_iss: iss.cloned(),
required_claims,
validate_signature,
validate_status_list,
status_list_cache: status_lists,
};
(validator, key)
}
pub(super) fn new_multi_issuer_tkn_validator<'a>(
iss: Option<&'a IssClaim>,
tkn_name: &'a str,
token_metadata: &TokenEntityMetadata,
algorithm: Algorithm,
status_lists: StatusListCache,
validate_signature: bool,
validate_status_list: bool,
) -> (Self, ValidatorInfo<'a>) {
let token_kind = TokenKind::AuthorizeMultiIssuer(Cow::Borrowed(tkn_name));
let mut validation = Validation::new(algorithm);
validation.validate_exp = true;
validation.validate_nbf = true;
validation.required_spec_claims.clear();
validation.validate_aud = false;
let required_claims = token_metadata.required_claims.iter().cloned().collect();
let key = ValidatorInfo {
iss,
token_kind,
algorithm,
};
let validator = JwtValidator {
validation,
expected_iss: iss.cloned(),
required_claims,
validate_signature,
validate_status_list,
status_list_cache: status_lists,
};
(validator, key)
}
pub(super) fn new_status_list_tkn_validator(
iss: Option<&'_ IssClaim>,
status_list_uri: Option<String>,
algorithm: Algorithm,
validate_signature: bool,
) -> (Self, ValidatorInfo<'_>) {
let token_kind = TokenKind::StatusList;
let mut validation = Validation::new(algorithm);
validation.validate_exp = true;
validation.validate_nbf = true;
validation.required_spec_claims.clear();
validation.validate_aud = false;
validation.sub = status_list_uri;
let required_claims = ["sub", "iat", "status_list"]
.into_iter()
.map(std::convert::Into::into)
.collect();
let key = ValidatorInfo {
iss,
token_kind,
algorithm,
};
let validator = JwtValidator {
validation,
expected_iss: iss.cloned(),
required_claims,
validate_signature,
validate_status_list: false,
status_list_cache: StatusListCache::default(),
};
(validator, key)
}
pub(crate) fn validate_jwt(
&self,
jwt: &str,
decoding_key: Option<Arc<DecodingKey>>,
) -> Result<ValidatedJwt, ValidateJwtError> {
let validated_jwt = if self.validate_signature {
let Some(decoding_key) = decoding_key else {
return Err(ValidateJwtError::MissingValidationKey);
};
jwt::decode::<ValidatedJwt>(jwt, decoding_key.as_ref(), &self.validation)?.claims
} else {
let validated_jwt = jwt::dangerous::insecure_decode::<ValidatedJwt>(jwt)?.claims;
self.validate_claims_without_signature(&validated_jwt)?;
validated_jwt
};
self.validate_iss(&validated_jwt.claims)?;
let missing_claims = self
.required_claims
.iter()
.filter(|claim| validated_jwt.claims.get(claim).is_none())
.cloned()
.collect::<Vec<String>>();
if !missing_claims.is_empty() {
Err(ValidateJwtError::MissingClaims(missing_claims))?;
}
if self.validate_status_list {
let Some(ref_status_list) = validated_jwt.get_ref_status()? else {
return Ok(validated_jwt);
};
let jwt_status = {
self.status_list_cache
.status_lists
.read()
.expect("obtain status list read lock")
.get(&ref_status_list.uri)
.ok_or(ValidateJwtError::MissingStatusList)?
.get_status(ref_status_list.idx)?
};
if !jwt_status.is_valid() {
return Err(ValidateJwtError::RejectJwtStatus(jwt_status));
}
}
Ok(validated_jwt)
}
fn validate_claims_without_signature(
&self,
validated_jwt: &ValidatedJwt,
) -> Result<(), ValidateJwtError> {
let now = jwt::get_current_timestamp();
let claims = &validated_jwt.claims;
if self.validation.validate_exp
&& let Some(exp) = claims.get("exp")
{
let exp = exp.as_u64().ok_or_else(|| {
ValidateJwtError::ValidateJwt(new_error(ErrorKind::InvalidClaimFormat(
"exp".to_string(),
)))
})?;
if exp < self.validation.reject_tokens_expiring_in_less_than {
return Err(ValidateJwtError::ValidateJwt(new_error(
ErrorKind::InvalidToken,
)));
}
if exp - self.validation.reject_tokens_expiring_in_less_than
< now.saturating_sub(self.validation.leeway)
{
return Err(ValidateJwtError::ValidateJwt(new_error(
ErrorKind::ExpiredSignature,
)));
}
}
if self.validation.validate_nbf
&& let Some(nbf) = claims.get("nbf")
{
let nbf = nbf.as_u64().ok_or_else(|| {
ValidateJwtError::ValidateJwt(new_error(ErrorKind::InvalidClaimFormat(
"nbf".to_string(),
)))
})?;
if nbf > now.saturating_add(self.validation.leeway) {
return Err(ValidateJwtError::ValidateJwt(new_error(
ErrorKind::ImmatureSignature,
)));
}
}
if let (Some(sub), Some(expected_sub)) = (
claims.get("sub").and_then(Value::as_str),
self.validation.sub.as_deref(),
) && sub != expected_sub
{
return Err(ValidateJwtError::ValidateJwt(new_error(
ErrorKind::InvalidSubject,
)));
}
Ok(())
}
fn validate_iss(&self, claims: &Value) -> Result<(), ValidateJwtError> {
let Some(expected) = self.expected_iss.as_ref() else {
return Ok(());
};
let invalid = || ValidateJwtError::ValidateJwt(new_error(ErrorKind::InvalidIssuer));
let matched = match claims.get("iss") {
Some(Value::String(iss)) => IssClaim::new(iss) == *expected,
Some(Value::Array(iss_values)) => iss_values
.iter()
.filter_map(Value::as_str)
.any(|iss| IssClaim::new(iss) == *expected),
_ => false,
};
if matched { Ok(()) } else { Err(invalid()) }
}
}
impl DecodedJwt {
pub(crate) fn iss(&self) -> Option<IssClaim> {
self.claims
.inner
.get("iss")
.and_then(|x| x.as_str())
.map(IssClaim::new)
}
pub(crate) fn decoding_key_info(&self) -> DecodingKeyInfo {
DecodingKeyInfo {
issuer: self.iss(),
kid: self.header.kid.clone(),
algorithm: self.header.alg,
}
}
}
impl TryFrom<DecodedJwt> for ValidatedJwt {
type Error = serde_json::Error;
fn try_from(decoded_jwt: DecodedJwt) -> Result<Self, Self::Error> {
Ok(Self {
claims: decoded_jwt.claims.inner,
trusted_iss: None,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum ValidateJwtError {
#[error("failed to decode the JWT: {0}")]
DecodeJwt(#[from] DecodeJwtError),
#[error("failed to validate the JWT since no key was available")]
MissingValidationKey,
#[error(
"failed to validate JWT {0:?}: no validator was initialized. this may be due to an untrusted issuer or an unsupported algorithm"
)]
MissingValidator(OwnedValidatorInfo),
#[error("failed to validate the JWT: {0}")]
ValidateJwt(#[from] jwt::errors::Error),
#[error("validation failed since the JWT is missing the following required claims: {0:#?}")]
MissingClaims(Vec<String>),
#[error("failed to get the status for the JWT: {0}")]
GetJwtStatus(#[from] JwtStatusError),
#[error("the token is rejected because it's status is: {0}")]
RejectJwtStatus(JwtStatus),
#[error("there isn't a status list available for the token")]
MissingStatusList,
#[error("failed to deserialize the JWT's status claim: {0}")]
DeserializeStatusClaim(#[from] serde_json::Error),
#[error("failed to validate the JWT's trusted issuer: {0}")]
TrustedIssuerValidation(#[source] TrustedIssuerError),
}
#[cfg(test)]
mod test {
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use crate::common::issuer_utils::IssClaim;
use crate::common::policy_store::TokenEntityMetadata;
use crate::jwt::status_list::{JwtStatus, StatusBitSize, StatusList};
use crate::jwt::validation::{JwtValidator, ValidateJwtError, ValidatedJwt};
use crate::jwt::{StatusListCache, test_utils::*};
use jsonwebtoken::Algorithm;
use serde_json::json;
use test_utils::assert_eq;
#[track_caller]
fn generate_keys() -> KeyPair {
generate_keypair_hs256(Some("some_hs256_key")).expect("Should generate keys")
}
static TEST_TKN_ENTITY_METADATA: LazyLock<TokenEntityMetadata> =
LazyLock::new(|| TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::AccessToken".into(),
token_id: "jti".into(),
required_claims: HashSet::from(["exp".into(), "nbf".into()]),
});
#[test]
fn can_decode_jwt_without_sig_validation() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022,
"exp": u64::MAX,
"nbf": u64::MIN,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
false,
false,
);
let result = validator
.validate_jwt(&token, Some(decoding_key))
.expect("should validate JWT");
let expected = ValidatedJwt {
claims,
trusted_iss: None,
};
assert_eq!(result, expected);
}
#[test]
fn decoding_errors_if_token_is_expired_when_without_sig_validation() {
let iss = "127.0.0.1";
let keys = generate_keys();
let claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022,
"exp": 0,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let mut tkn_entity_metadata = TEST_TKN_ENTITY_METADATA.clone();
tkn_entity_metadata.required_claims = HashSet::from(["exp".into()]);
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
false,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("should error due to expired JWT");
assert!(matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ExpiredSignature
));
}
#[test]
fn can_decode_and_validate_jwt() {
let iss = "127.0.0.1";
let keys = generate_keys();
let claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 0,
"nbf": 10,
"exp": u64::MAX,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let result = validator
.validate_jwt(&token, Some(decoding_key))
.expect("Should successfully process JWT");
let expected = ValidatedJwt {
claims,
trusted_iss: None,
};
assert_eq!(result, expected);
}
#[test]
fn validates_jwt_when_token_iss_has_trailing_slash() {
let keys = generate_keys();
let expected_iss = "https://dev-vci4e3lpvw2symco231.eu.auth0.com";
let token_iss = "https://dev-vci4e3lpvw2symco231.eu.auth0.com/";
let claims = json!({
"iss": token_iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 0,
"nbf": 10,
"exp": u64::MAX,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(expected_iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let result = validator
.validate_jwt(&token, Some(decoding_key))
.expect("trailing-slash iss must validate against canonical expected iss");
let expected = ValidatedJwt {
claims,
trusted_iss: None,
};
assert_eq!(
result, expected,
"validate_jwt should accept trailing-slash iss and produce expected ValidatedJwt with claims and no trusted_iss"
);
}
#[test]
fn rejects_token_missing_iss_when_expected_iss_configured() {
let keys = generate_keys();
let expected_iss = "https://issuer.example.com";
let claims_missing = json!({
"sub": "1234567890",
"iat": 0,
"nbf": 10,
"exp": u64::MAX,
});
let token_missing = generate_token_using_claims(&claims_missing, &keys)
.expect("Should generate token using keys");
let claims_non_string = json!({
"iss": 42,
"sub": "1234567890",
"iat": 0,
"nbf": 10,
"exp": u64::MAX,
});
let token_non_string = generate_token_using_claims(&claims_non_string, &keys)
.expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(expected_iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
for token in [token_missing, token_non_string] {
let err = validator
.validate_jwt(&token, Some(decoding_key.clone()))
.expect_err("token without valid iss must be rejected");
assert!(
matches!(
err,
ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::InvalidIssuer
),
"expected InvalidIssuer, got {err:?}"
);
}
}
#[test]
fn errors_on_expired_token() {
let iss = "127.0.0.1";
let keys = generate_keys();
let claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022,
"exp": 0,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let mut tkn_entity_metadata = TEST_TKN_ENTITY_METADATA.clone();
tkn_entity_metadata.required_claims = HashSet::from(["exp".into(), "nbf".into()]);
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("should error when validating JWT");
assert!(
matches!(
err,
ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ExpiredSignature
),
"expected validation to fail due to the token being expired."
);
}
#[test]
fn errors_on_immature_token() {
let iss = "127.0.0.1";
let keys = generate_keys();
let claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022,
"nbf": u64::MAX,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("should error when validating JWT");
assert!(
matches!(
err,
ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ImmatureSignature
),
"expected validation to fail due to the token being immature."
);
}
#[test]
fn can_check_missing_claims() {
let iss = "127.0.0.1";
let keys = generate_keys();
let claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let mut tkn_entity_metadata = TEST_TKN_ENTITY_METADATA.clone();
tkn_entity_metadata.required_claims =
HashSet::from(["sub", "name", "iat"].map(std::convert::Into::into));
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_entity_metadata,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let result = validator
.validate_jwt(&token, Some(decoding_key.clone()))
.expect("Should process JWT successfully");
let expected = ValidatedJwt {
claims,
trusted_iss: None,
};
assert_eq!(result, expected);
let mut tkn_entity_metadata = TEST_TKN_ENTITY_METADATA.clone();
tkn_entity_metadata.required_claims =
HashSet::from(["sub", "name", "iat", "nbf"].map(std::convert::Into::into));
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_entity_metadata,
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("expected an error while validating the JWT");
assert!(
matches!(
err,
ValidateJwtError::MissingClaims(missing_claims)
if missing_claims == vec!["nbf".to_string()]
),
"expected an error due to missing `nbf` claim"
);
}
#[tokio::test]
async fn reject_invalid_token_from_status_list() {
let bit_size = StatusBitSize::try_from(1u8).unwrap();
let status_list = [0b1111_1111];
let mut server = MockServer::new_with_defaults().await.unwrap();
server.generate_status_list_endpoint(bit_size, &status_list, None);
let iss = server.issuer();
let decoding_key = server.jwt_decoding_key().unwrap();
let mut claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 0,
"nbf": 10,
"exp": u64::MAX,
});
let token = server
.generate_token_with_hs256sig(&mut claims, Some(0))
.unwrap();
let status_lists: StatusListCache = HashMap::from([(
server.status_list_endpoint().unwrap().to_string(),
StatusList {
bit_size,
list: status_list.to_vec(),
},
)])
.into();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&iss),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
status_lists,
true,
true,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("should error because the status of the token is JwtStatus::Invalid");
assert!(
matches!(
err,
ValidateJwtError::RejectJwtStatus(ref status)
if *status == JwtStatus::Invalid
),
"GOT {err:?}: {err}"
);
}
fn tkn_meta_no_required_claims() -> TokenEntityMetadata {
TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::AccessToken".into(),
token_id: "jti".into(),
required_claims: HashSet::new(),
}
}
#[test]
fn rejects_expired_token_without_exp_in_required_claims_no_sig() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"iat": 1_516_239_022,
"exp": 0,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_meta_no_required_claims(),
Algorithm::HS256,
StatusListCache::default(),
false,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err(
"expired token should be rejected even when 'exp' is not in required_claims",
);
assert!(
matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ExpiredSignature),
"expected ExpiredSignature, got {err:?}"
);
}
#[test]
fn rejects_expired_token_without_exp_in_required_claims_with_sig() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"iat": 1_516_239_022,
"exp": 0,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_meta_no_required_claims(),
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err(
"expired token should be rejected even when 'exp' is not in required_claims",
);
assert!(
matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ExpiredSignature),
"expected ExpiredSignature, got {err:?}"
);
}
#[test]
fn rejects_immature_token_without_nbf_in_required_claims_no_sig() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"iat": 1_516_239_022,
"nbf": u64::MAX,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_meta_no_required_claims(),
Algorithm::HS256,
StatusListCache::default(),
false,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err(
"immature token should be rejected even when 'nbf' is not in required_claims",
);
assert!(
matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ImmatureSignature),
"expected ImmatureSignature, got {err:?}"
);
}
#[test]
fn rejects_immature_token_without_nbf_in_required_claims_with_sig() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"iat": 1_516_239_022,
"nbf": u64::MAX,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_meta_no_required_claims(),
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err(
"immature token should be rejected even when 'nbf' is not in required_claims",
);
assert!(
matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ImmatureSignature),
"expected ImmatureSignature, got {err:?}"
);
}
#[test]
fn rejects_expired_token_multi_issuer_without_exp_in_required_claims() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"iat": 1_516_239_022,
"exp": 0,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_multi_issuer_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_meta_no_required_claims(),
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("expired token should be rejected by multi-issuer validator even when 'exp' is not in required_claims");
assert!(
matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ExpiredSignature),
"expected ExpiredSignature, got {err:?}"
);
}
#[test]
fn rejects_immature_token_multi_issuer_without_nbf_in_required_claims() {
let keys = generate_keys();
let iss = "127.0.0.1";
let claims = json!({
"iss": iss,
"sub": "1234567890",
"iat": 1_516_239_022,
"nbf": u64::MAX,
});
let token =
generate_token_using_claims(&claims, &keys).expect("Should generate token using keys");
let decoding_key = keys.decoding_key().unwrap();
let (validator, _) = JwtValidator::new_multi_issuer_tkn_validator(
Some(&IssClaim::new(iss)),
"access_token",
&tkn_meta_no_required_claims(),
Algorithm::HS256,
StatusListCache::default(),
true,
false,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("immature token should be rejected by multi-issuer validator even when 'nbf' is not in required_claims");
assert!(
matches!(err, ValidateJwtError::ValidateJwt(ref e)
if *e.kind() == jsonwebtoken::errors::ErrorKind::ImmatureSignature),
"expected ImmatureSignature, got {err:?}"
);
}
#[tokio::test]
async fn reject_suspended_token_from_status_list() {
let bit_size = StatusBitSize::try_from(1u8).unwrap();
let status_list = [0b1111_1111];
let mut server = MockServer::new_with_defaults().await.unwrap();
server.generate_status_list_endpoint(bit_size, &status_list, None);
let iss = server.issuer();
let decoding_key = server.jwt_decoding_key().unwrap();
let mut claims = json!({
"iss": iss,
"sub": "1234567890",
"name": "John Doe",
"iat": 0,
"nbf": 10,
"exp": u64::MAX,
});
let token = server
.generate_token_with_hs256sig(&mut claims, Some(0))
.unwrap();
let status_lists: StatusListCache = HashMap::from([(
server.status_list_endpoint().unwrap().to_string(),
StatusList {
bit_size: 2u8.try_into().unwrap(),
list: vec![0b1010_1010],
},
)])
.into();
let (validator, _) = JwtValidator::new_input_tkn_validator(
Some(&iss),
"access_token",
&TEST_TKN_ENTITY_METADATA,
Algorithm::HS256,
status_lists,
true,
true,
);
let err = validator
.validate_jwt(&token, Some(decoding_key))
.expect_err("should error because the status of the token is JwtStatus::Suspended");
assert!(
matches!(
err,
ValidateJwtError::RejectJwtStatus(ref status)
if *status == JwtStatus::Suspended
),
"GOT {err:?}: {err}"
);
}
}