#![warn(clippy::pedantic)]
#![allow(
clippy::module_name_repetitions,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::default_trait_access,
clippy::similar_names,
clippy::enum_glob_use
)]
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::{Duration, OffsetDateTime};
mod helpers;
pub use crate::helpers::*;
#[cfg(test)]
#[macro_use]
mod test;
#[macro_use]
mod serde_custom;
#[macro_use]
mod macros;
pub mod errors;
pub mod jwa;
pub mod jwe;
pub mod jwk;
pub mod jws;
use crate::errors::{Error, ValidationError};
pub trait FromCompactPart: Sized {
fn from_bytes(b: &[u8]) -> Result<Self, Error>;
}
pub trait ToCompactPart: Sized {
fn to_bytes(&self) -> Result<Cow<'_, [u8]>, Error>;
}
pub struct Json<T>(pub T);
impl<T: DeserializeOwned> FromCompactPart for Json<T> {
fn from_bytes(b: &[u8]) -> Result<Self, Error> {
Ok(Json(serde_json::from_slice(b)?))
}
}
impl<T: Serialize> ToCompactPart for Json<T> {
fn to_bytes(&self) -> Result<Cow<'_, [u8]>, Error> {
Ok(serde_json::to_vec(&self.0)?.into())
}
}
impl FromCompactPart for Vec<u8> {
fn from_bytes(b: &[u8]) -> Result<Self, Error> {
Ok(b.to_vec())
}
}
impl ToCompactPart for Vec<u8> {
fn to_bytes(&self) -> Result<Cow<'_, [u8]>, Error> {
Ok(self.as_slice().into())
}
}
impl FromCompactPart for () {
fn from_bytes(b: &[u8]) -> Result<Self, Error> {
if b.is_empty() {
Ok(())
} else {
Err(Error::DecodeError(errors::DecodeError::PartsLengthError {
expected: 0,
actual: b.len(),
}))
}
}
}
impl ToCompactPart for () {
fn to_bytes(&self) -> Result<Cow<'_, [u8]>, Error> {
Ok(Cow::Borrowed(&[]))
}
}
pub type JWT<T> = jws::Verified<ClaimsSet<T>, ()>;
pub type JWE<T> = jwe::Decrypted<jws::Unverified<ClaimsSet<T>>, ()>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SingleOrMultiple {
Single([String; 1]),
Multiple(Vec<String>),
}
mod serde_impls {
use super::SingleOrMultiple;
impl serde::Serialize for SingleOrMultiple {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
SingleOrMultiple::Single([field]) => field.serialize(serializer),
SingleOrMultiple::Multiple(ref field) => field.serialize(serializer),
}
}
}
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = SingleOrMultiple;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("single or multiple strings")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.into())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.into())
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = Vec::new();
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(SingleOrMultiple::Multiple(values))
}
}
impl<'de> serde::Deserialize<'de> for SingleOrMultiple {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(Visitor)
}
}
}
impl From<&str> for SingleOrMultiple {
fn from(t: &str) -> Self {
Self::Single([t.to_owned()])
}
}
impl From<&[&str]> for SingleOrMultiple {
fn from(t: &[&str]) -> Self {
Self::Multiple(t.iter().map(|&s| s.to_owned()).collect())
}
}
impl From<String> for SingleOrMultiple {
fn from(t: String) -> Self {
Self::Single([t])
}
}
impl From<Vec<String>> for SingleOrMultiple {
fn from(t: Vec<String>) -> Self {
Self::Multiple(t)
}
}
impl SingleOrMultiple {
pub fn contains(&self, value: &str) -> bool {
match self {
Self::Single([single]) => single == value,
Self::Multiple(vector) => vector.iter().any(|v| v == value),
}
}
pub fn iter(&self) -> std::slice::Iter<String> {
match self {
Self::Single(single) => single.iter(),
Self::Multiple(vector) => vector.iter(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Timestamp(OffsetDateTime);
impl Deref for Timestamp {
type Target = OffsetDateTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<OffsetDateTime> for Timestamp {
fn from(datetime: OffsetDateTime) -> Self {
Timestamp(datetime)
}
}
impl From<Timestamp> for OffsetDateTime {
fn from(ts: Timestamp) -> Self {
ts.0
}
}
impl TryFrom<i64> for Timestamp {
type Error = time::error::ComponentRange;
fn try_from(value: i64) -> Result<Self, Self::Error> {
OffsetDateTime::from_unix_timestamp(value).map(Self)
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
time::serde::timestamp::serialize(&self.0, serializer)
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
time::serde::timestamp::deserialize(deserializer).map(Self)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
pub struct RegisteredClaims {
#[serde(rename = "iss", skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(rename = "aud", skip_serializing_if = "Option::is_none")]
pub audience: Option<SingleOrMultiple>,
#[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
pub expiry: Option<Timestamp>,
#[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
pub not_before: Option<Timestamp>,
#[serde(rename = "iat", skip_serializing_if = "Option::is_none")]
pub issued_at: Option<Timestamp>,
#[serde(rename = "jti", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)]
pub struct ClaimPresenceOptions {
pub issued_at: Presence,
pub not_before: Presence,
pub expiry: Presence,
pub issuer: Presence,
pub audience: Presence,
pub subject: Presence,
pub id: Presence,
}
impl ClaimPresenceOptions {
pub fn strict() -> Self {
use Presence::Required;
ClaimPresenceOptions {
issued_at: Required,
not_before: Required,
expiry: Required,
issuer: Required,
audience: Required,
subject: Required,
id: Required,
}
}
}
#[derive(Eq, PartialEq, Clone)]
pub struct ValidationOptions {
pub claim_presence_options: ClaimPresenceOptions,
pub temporal_options: TemporalOptions,
pub issued_at: Validation<Duration>,
pub not_before: Validation<()>,
pub expiry: Validation<()>,
pub issuer: Validation<String>,
pub audience: Validation<String>,
}
impl Default for ValidationOptions {
fn default() -> Self {
ValidationOptions {
expiry: Validation::Validate(()),
not_before: Validation::Validate(()),
issued_at: Validation::Validate(Duration::MAX),
claim_presence_options: Default::default(),
temporal_options: Default::default(),
audience: Default::default(),
issuer: Default::default(),
}
}
}
impl RegisteredClaims {
pub fn validate_claim_presence(
&self,
options: ClaimPresenceOptions,
) -> Result<(), ValidationError> {
use crate::Presence::Required;
let mut missing_claims: Vec<&str> = vec![];
if options.expiry == Required && self.expiry.is_none() {
missing_claims.push("exp");
}
if options.not_before == Required && self.not_before.is_none() {
missing_claims.push("nbf");
}
if options.issued_at == Required && self.issued_at.is_none() {
missing_claims.push("iat");
}
if options.audience == Required && self.audience.is_none() {
missing_claims.push("aud");
}
if options.issuer == Required && self.issuer.is_none() {
missing_claims.push("iss");
}
if options.subject == Required && self.subject.is_none() {
missing_claims.push("sub");
}
if options.id == Required && self.id.is_none() {
missing_claims.push("jti");
}
if missing_claims.is_empty() {
Ok(())
} else {
Err(ValidationError::MissingRequiredClaims(
missing_claims.into_iter().map(Into::into).collect(),
))
}
}
pub fn validate_exp(
&self,
validation: Validation<TemporalOptions>,
) -> Result<(), ValidationError> {
match validation {
Validation::Ignored => Ok(()),
Validation::Validate(temporal_options) => {
let now = temporal_options.now.unwrap_or_else(OffsetDateTime::now_utc);
match self.expiry {
Some(Timestamp(expiry)) if now - expiry > temporal_options.epsilon => {
Err(ValidationError::Expired(now - expiry))
}
_ => Ok(()),
}
}
}
}
pub fn validate_nbf(
&self,
validation: Validation<TemporalOptions>,
) -> Result<(), ValidationError> {
match validation {
Validation::Ignored => Ok(()),
Validation::Validate(temporal_options) => {
let now = temporal_options.now.unwrap_or_else(OffsetDateTime::now_utc);
match self.not_before {
Some(Timestamp(nbf)) if nbf - now > temporal_options.epsilon => {
Err(ValidationError::NotYetValid(nbf - now))
}
_ => Ok(()),
}
}
}
}
pub fn validate_iat(
&self,
validation: Validation<(Duration, TemporalOptions)>,
) -> Result<(), ValidationError> {
match validation {
Validation::Ignored => Ok(()),
Validation::Validate((max_age, temporal_options)) => {
let now = temporal_options.now.unwrap_or_else(OffsetDateTime::now_utc);
match self.issued_at {
Some(Timestamp(iat)) if iat - now > temporal_options.epsilon => {
Err(ValidationError::NotYetValid(iat - now))
}
Some(Timestamp(iat)) if now - iat > max_age - temporal_options.epsilon => {
Err(ValidationError::TooOld(now - iat - max_age))
}
_ => Ok(()),
}
}
}
}
pub fn validate_aud(&self, validation: Validation<String>) -> Result<(), ValidationError> {
match validation {
Validation::Ignored => Ok(()),
Validation::Validate(expected_aud) => match &self.audience {
Some(SingleOrMultiple::Single([audience])) if audience != &expected_aud => Err(
ValidationError::InvalidAudience(SingleOrMultiple::Single([audience.clone()])),
),
Some(SingleOrMultiple::Multiple(audiences))
if !audiences.contains(&expected_aud) =>
{
Err(ValidationError::InvalidAudience(
SingleOrMultiple::Multiple(audiences.clone()),
))
}
_ => Ok(()),
},
}
}
pub fn validate_iss(&self, validation: Validation<String>) -> Result<(), ValidationError> {
match validation {
Validation::Ignored => Ok(()),
Validation::Validate(expected_issuer) => match self.issuer {
Some(ref iss) if iss != &expected_issuer => {
Err(ValidationError::InvalidIssuer(iss.clone()))
}
_ => Ok(()),
},
}
}
pub fn validate(&self, options: ValidationOptions) -> Result<(), ValidationError> {
self.validate_claim_presence(options.claim_presence_options)?;
self.validate_exp(options.expiry.map(|_| options.temporal_options))?;
self.validate_nbf(options.not_before.map(|_| options.temporal_options))?;
self.validate_iat(options.issued_at.map(|dur| (dur, options.temporal_options)))?;
self.validate_iss(options.issuer)?;
self.validate_aud(options.audience)?;
Ok(())
}
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct ClaimsSet<T> {
#[serde(flatten)]
pub registered: RegisteredClaims,
#[serde(flatten)]
pub private: T,
}
impl<T: DeserializeOwned> FromCompactPart for ClaimsSet<T> {
fn from_bytes(b: &[u8]) -> Result<Self, Error> {
Ok(serde_json::from_slice(b)?)
}
}
impl<T: Serialize> ToCompactPart for ClaimsSet<T> {
fn to_bytes(&self) -> Result<Cow<'_, [u8]>, Error> {
Ok(serde_json::to_vec(&self)?.into())
}
}
type B64 = base64ct::Base64UrlUnpadded;
#[cfg(test)]
mod tests {
use super::*;
use time::Duration;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct PrivateClaims {
company: String,
department: String,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct InvalidPrivateClaim {
sub: String,
company: String,
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
struct SingleOrMultipleStrings {
values: SingleOrMultiple,
}
#[test]
fn single_string_serialization_round_trip() {
let test = SingleOrMultipleStrings {
values: "foobar".into(),
};
let expected_json = r#"{"values":"foobar"}"#;
let serialized = serde_json::to_string(&test).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, test);
assert!(deserialized.values.contains("foobar"));
assert!(!deserialized.values.contains("does not exist"));
}
#[test]
fn multiple_strings_serialization_round_trip() {
let test = SingleOrMultipleStrings {
values: ["foo", "bar", "baz"].as_slice().into(),
};
let expected_json = r#"{"values":["foo","bar","baz"]}"#;
let serialized = serde_json::to_string(&test).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, test);
assert!(deserialized.values.contains("foo"));
assert!(deserialized.values.contains("bar"));
assert!(deserialized.values.contains("baz"));
assert!(!deserialized.values.contains("does not exist"));
}
#[test]
fn single_string_or_uri_string_serialization_round_trip() {
let test = SingleOrMultipleStrings {
values: "foobar".into(),
};
let expected_json = r#"{"values":"foobar"}"#;
let serialized = serde_json::to_string(&test).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, test);
assert!(deserialized.values.contains("foobar"));
assert!(!deserialized.values.contains("does not exist"));
}
#[test]
fn single_string_or_uri_uri_serialization_round_trip() {
let test = SingleOrMultipleStrings {
values: "https://www.examples.com/".into(),
};
let expected_json = r#"{"values":"https://www.examples.com/"}"#;
let serialized = serde_json::to_string(&test).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, test);
assert!(deserialized.values.contains("https://www.examples.com/"));
assert!(!deserialized.values.contains("https://ecorp.com"));
}
#[test]
fn multiple_string_or_uri_serialization_round_trip() {
let test = SingleOrMultipleStrings {
values: [
"foo",
"https://www.example.com/",
"data:text/plain,Hello?World#",
"http://[::1]/",
"baz",
]
.as_slice()
.into(),
};
let expected_json = r#"{"values":["foo","https://www.example.com/","data:text/plain,Hello?World#","http://[::1]/","baz"]}"#;
let serialized = serde_json::to_string(&test).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, test);
assert!(deserialized.values.contains("foo"));
assert!(deserialized.values.contains("https://www.example.com/"));
assert!(deserialized.values.contains("data:text/plain,Hello?World#"));
assert!(deserialized.values.contains("http://[::1]/"));
assert!(deserialized.values.contains("baz"));
assert!(!deserialized.values.contains("https://ecorp.com"));
}
#[test]
fn timestamp_serialization_roundtrip() {
let now: Timestamp = OffsetDateTime::now_utc()
.replace_nanosecond(0)
.unwrap()
.into();
let serialized = serde_json::to_string(&now).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
assert_eq!(now, deserialized);
let fixed_time: Timestamp = 1000.try_into().unwrap();
let serialized = serde_json::to_string(&fixed_time).unwrap();
assert_eq!(serialized, "1000");
let deserialized = serde_json::from_str(&serialized).unwrap();
assert_eq!(fixed_time, deserialized);
}
#[test]
fn empty_registered_claims_serialization_round_trip() {
let claim = RegisteredClaims::default();
let expected_json = "{}";
let serialized = serde_json::to_string(&claim).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: RegisteredClaims = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, claim);
}
#[test]
fn registered_claims_serialization_round_trip() {
let claim = RegisteredClaims {
issuer: Some("https://www.acme.com/".into()),
audience: Some("htts://acme-customer.com/".into()),
not_before: Some(1234.try_into().unwrap()),
..Default::default()
};
let expected_json =
r#"{"iss":"https://www.acme.com/","aud":"htts://acme-customer.com/","nbf":1234}"#;
let serialized = serde_json::to_string(&claim).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: RegisteredClaims = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, claim);
}
#[test]
fn claims_set_serialization_round_trip() {
let claim = ClaimsSet::<PrivateClaims> {
registered: RegisteredClaims {
issuer: Some("https://www.acme.com/".into()),
subject: Some("John Doe".into()),
audience: Some("htts://acme-customer.com/".into()),
not_before: Some(1234.try_into().unwrap()),
..Default::default()
},
private: PrivateClaims {
department: "Toilet Cleaning".to_string(),
company: "ACME".to_string(),
},
};
let expected_json = "{\"iss\":\"https://www.acme.com/\",\"sub\":\"John Doe\",\
\"aud\":\"htts://acme-customer.com/\",\
\"nbf\":1234,\"company\":\"ACME\",\"department\":\"Toilet Cleaning\"}";
let serialized = serde_json::to_string(&claim).unwrap();
assert_eq!(expected_json, serialized);
let deserialized: ClaimsSet<PrivateClaims> = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, claim);
}
#[test]
fn duplicate_claims_round_trip() {
let claim = ClaimsSet::<InvalidPrivateClaim> {
registered: RegisteredClaims {
issuer: Some("https://www.acme.com".into()),
subject: Some("John Doe".into()),
audience: Some("htts://acme-customer.com".into()),
not_before: Some(1234.try_into().unwrap()),
..Default::default()
},
private: InvalidPrivateClaim {
sub: "John Doe".to_string(),
company: "ACME".to_string(),
},
};
let json = serde_json::to_string(&claim).unwrap();
assert_eq!(2, json.matches("\"sub\"").count());
let duplicate: Result<ClaimsSet<InvalidPrivateClaim>, _> = serde_json::from_str(&json);
assert!(duplicate.is_err());
let error = duplicate.unwrap_err().to_string();
assert!(error.contains("duplicate field `sub`"));
}
#[test]
#[should_panic(expected = "MissingRequiredClaims([\"iat\"])")]
fn validate_times_missing_iat() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions {
issued_at: Presence::Required,
..Default::default()
};
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
#[should_panic(expected = "MissingRequiredClaims([\"exp\"])")]
fn validate_times_missing_exp() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions {
expiry: Presence::Required,
..Default::default()
};
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
#[should_panic(expected = "MissingRequiredClaims([\"nbf\"])")]
fn validate_times_missing_nbf() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions {
not_before: Presence::Required,
..Default::default()
};
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
#[should_panic(expected = "MissingRequiredClaims([\"aud\"])")]
fn validate_times_missing_aud() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions {
audience: Presence::Required,
..Default::default()
};
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
#[should_panic(expected = "MissingRequiredClaims([\"iss\"])")]
fn validate_times_missing_iss() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions {
issuer: Presence::Required,
..Default::default()
};
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
#[should_panic(expected = "MissingRequiredClaims([\"sub\"])")]
fn validate_times_missing_sub() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions {
subject: Presence::Required,
..Default::default()
};
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
#[should_panic(
expected = "MissingRequiredClaims([\"exp\", \"nbf\", \"iat\", \"aud\", \"iss\", \"sub\", \"jti\"])"
)]
fn validate_times_missing_all() {
let registered_claims = RegisteredClaims::default();
let options = ClaimPresenceOptions::strict();
registered_claims.validate_claim_presence(options).unwrap();
}
#[test]
fn validate_times_catch_future_token() {
let temporal_options = TemporalOptions {
now: Some(OffsetDateTime::from_unix_timestamp(0).unwrap()),
..Default::default()
};
let registered_claims = RegisteredClaims {
issued_at: Some(10.try_into().unwrap()),
..Default::default()
};
assert_eq!(
Err(ValidationError::NotYetValid(Duration::seconds(10))),
registered_claims.validate_iat(Validation::Validate((
Duration::seconds(0),
temporal_options
)))
);
}
#[test]
fn validate_times_catch_too_old_token() {
let temporal_options = TemporalOptions {
now: Some(OffsetDateTime::from_unix_timestamp(40).unwrap()),
..Default::default()
};
let registered_claims = RegisteredClaims {
issued_at: Some(10.try_into().unwrap()),
..Default::default()
};
assert_eq!(
Err(ValidationError::TooOld(Duration::seconds(5))),
registered_claims.validate_iat(Validation::Validate((
Duration::seconds(25),
temporal_options
)))
);
}
#[test]
fn validate_times_catch_expired_token() {
let temporal_options = TemporalOptions {
now: Some(OffsetDateTime::from_unix_timestamp(2).unwrap()),
..Default::default()
};
let registered_claims = RegisteredClaims {
expiry: Some(1.try_into().unwrap()),
..Default::default()
};
assert_eq!(
Err(ValidationError::Expired(Duration::seconds(1))),
registered_claims.validate_exp(Validation::Validate(temporal_options))
);
}
#[test]
fn validate_times_catch_early_token() {
let temporal_options = TemporalOptions {
now: Some(OffsetDateTime::from_unix_timestamp(0).unwrap()),
..Default::default()
};
let registered_claims = RegisteredClaims {
not_before: Some(1.try_into().unwrap()),
..Default::default()
};
assert_eq!(
Err(ValidationError::NotYetValid(Duration::seconds(1))),
registered_claims.validate_nbf(Validation::Validate(temporal_options))
);
}
#[test]
fn validate_times_valid_token_with_default_options() {
let registered_claims = RegisteredClaims {
not_before: Some(Timestamp(OffsetDateTime::now_utc() - Duration::days(2))),
issued_at: Some(Timestamp(OffsetDateTime::now_utc() - Duration::days(1))),
expiry: Some(Timestamp(OffsetDateTime::now_utc() + Duration::days(1))),
..Default::default()
};
let validation_options = ValidationOptions {
temporal_options: Default::default(),
claim_presence_options: Default::default(),
expiry: Validation::Validate(()),
not_before: Validation::Validate(()),
issued_at: Validation::Validate(Duration::MAX),
..Default::default()
};
registered_claims.validate(validation_options).unwrap();
}
#[test]
fn validate_issuer_catch_mismatch() {
let registered_claims = RegisteredClaims {
issuer: Some("issuer".to_string()),
..Default::default()
};
assert_eq!(
Err(ValidationError::InvalidIssuer("issuer".to_string())),
registered_claims.validate_iss(Validation::Validate("http://issuer".to_string()))
);
}
#[test]
fn validate_audience_when_single() {
let aud: SingleOrMultiple = "audience".into();
let registered_claims = RegisteredClaims {
audience: Some(aud.clone()),
..Default::default()
};
assert_eq!(
Err(ValidationError::InvalidAudience(aud.clone())),
registered_claims.validate_aud(Validation::Validate("http://audience".to_string()))
);
assert_eq!(
Err(ValidationError::InvalidAudience(aud)),
registered_claims.validate_aud(Validation::Validate("audience2".to_string()))
);
assert_eq!(
Ok(()),
registered_claims.validate_aud(Validation::Validate("audience".to_string()))
);
}
#[test]
fn validate_audience_when_multiple() {
let aud =
SingleOrMultiple::Multiple(vec!["audience".to_string(), "http://audience".to_string()]);
let registered_claims = RegisteredClaims {
audience: Some(aud.clone()),
..Default::default()
};
assert_eq!(
Ok(()),
registered_claims.validate_aud(Validation::Validate("http://audience".to_string()))
);
assert_eq!(
Err(ValidationError::InvalidAudience(aud.clone())),
registered_claims.validate_aud(Validation::Validate("audience2".to_string()))
);
assert_eq!(
Err(ValidationError::InvalidAudience(aud)),
registered_claims.validate_aud(Validation::Validate("https://audience".to_string()))
);
assert_eq!(
Ok(()),
registered_claims.validate_aud(Validation::Validate("audience".to_string()))
);
}
#[test]
fn validate_valid_token_with_all_required() {
let registered_claims = RegisteredClaims {
expiry: Some(999.try_into().unwrap()),
not_before: Some(1.try_into().unwrap()),
issued_at: Some(95.try_into().unwrap()),
subject: Some("subject".to_string()),
issuer: Some("issuer".to_string()),
audience: Some(SingleOrMultiple::Multiple(vec![
"http://audience".to_string(),
"audience".to_string(),
])),
id: Some("id".into()),
};
let temporal_options = TemporalOptions {
now: Some(OffsetDateTime::from_unix_timestamp(100).unwrap()),
..Default::default()
};
let validation_options = ValidationOptions {
temporal_options,
claim_presence_options: ClaimPresenceOptions::strict(),
expiry: Validation::Validate(()),
not_before: Validation::Validate(()),
issued_at: Validation::Validate(Duration::MAX),
audience: Validation::Validate("audience".to_string()),
issuer: Validation::Validate("issuer".to_string()),
};
registered_claims.validate(validation_options).unwrap();
}
#[test]
fn validate_times_valid_token_with_epsilon() {
let registered_claims = RegisteredClaims {
expiry: Some(99.try_into().unwrap()),
not_before: Some(96.try_into().unwrap()),
issued_at: Some(96.try_into().unwrap()),
..Default::default()
};
let temporal_options = TemporalOptions {
now: Some(OffsetDateTime::from_unix_timestamp(100).unwrap()),
epsilon: Duration::seconds(10),
};
let validation_options = ValidationOptions {
temporal_options,
claim_presence_options: Default::default(),
expiry: Validation::Validate(()),
not_before: Validation::Validate(()),
issued_at: Validation::Validate(Duration::MAX),
..Default::default()
};
registered_claims.validate(validation_options).unwrap();
}
}