#![allow(
missing_copy_implementations,
missing_debug_implementations,
unknown_lints
)]
#![allow(clippy::try_err, clippy::needless_doctest_main)]
#![deny(
arithmetic_overflow,
bad_style,
const_err,
dead_code,
improper_ctypes,
missing_docs,
mutable_transmutes,
no_mangle_const_items,
non_camel_case_types,
non_shorthand_field_patterns,
non_upper_case_globals,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
stable_features,
trivial_casts,
trivial_numeric_casts,
unconditional_recursion,
unknown_crate_types,
unreachable_code,
unused_allocation,
unused_assignments,
unused_attributes,
unused_comparisons,
unused_extern_crates,
unused_features,
unused_import_braces,
unused_imports,
unused_must_use,
unused_mut,
unused_parens,
unused_qualifications,
unused_results,
unused_unsafe,
unused_variables,
variant_size_differences,
while_true
)]
#![doc(test(attr(allow(unused_variables), deny(warnings))))]
#![cfg_attr(feature = "strict", deny(warnings))]
#![cfg_attr(feature = "strict", allow(unused_braces))]
use std::borrow::Borrow;
use std::fmt::{self, Debug, Display};
use std::iter;
use std::ops::Deref;
use std::str::{self, FromStr};
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use data_encoding::BASE64URL_NOPAD;
use serde::de::{self, DeserializeOwned};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
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;
pub mod digest;
use crate::errors::{Error, ValidationError};
pub type JWT<T, H> = jws::Compact<ClaimsSet<T>, H>;
pub type JWE<T, H, I> = jwe::Compact<JWT<T, H>, I>;
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Default)]
pub struct Empty {}
impl CompactJson for Empty {}
pub trait CompactPart {
fn to_bytes(&self) -> Result<Vec<u8>, Error>;
fn from_bytes(bytes: &[u8]) -> Result<Self, Error>
where
Self: Sized;
fn from_base64<B: AsRef<[u8]>>(encoded: &B) -> Result<Self, Error>
where
Self: Sized,
{
let decoded = BASE64URL_NOPAD.decode(encoded.as_ref())?;
Self::from_bytes(&decoded)
}
fn to_base64(&self) -> Result<Base64Url, Error> {
let bytes = self.to_bytes()?;
Ok(Base64Url(BASE64URL_NOPAD.encode(bytes.as_ref())))
}
}
pub trait CompactJson: Serialize + DeserializeOwned {}
impl<T> CompactPart for T
where
T: CompactJson,
{
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
Ok(serde_json::to_vec(&self)?)
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
Ok(serde_json::from_slice(bytes)?)
}
}
impl CompactPart for Vec<u8> {
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
Ok(self.clone())
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
Ok(bytes.to_vec())
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Base64Url(String);
impl Base64Url {
pub fn unwrap(self) -> String {
let Base64Url(string) = self;
string
}
pub fn str(&self) -> &str {
&self.0
}
}
impl Deref for Base64Url {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl FromStr for Base64Url {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Base64Url(s.to_string()))
}
}
impl Borrow<str> for Base64Url {
fn borrow(&self) -> &str {
self.str()
}
}
impl CompactPart for Base64Url {
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
Ok(BASE64URL_NOPAD.decode(self.as_ref())?)
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let string = str::from_utf8(bytes)?;
Ok(Base64Url(string.to_string()))
}
fn to_base64(&self) -> Result<Base64Url, Error> {
Ok((*self).clone())
}
fn from_base64<B: AsRef<[u8]>>(encoded: &B) -> Result<Self, Error> {
Self::from_bytes(encoded.as_ref())
}
}
impl AsRef<[u8]> for Base64Url {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Compact {
pub parts: Vec<Base64Url>,
}
impl Compact {
pub fn new() -> Self {
Self { parts: vec![] }
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
parts: Vec::with_capacity(capacity),
}
}
pub fn push(&mut self, part: &dyn CompactPart) -> Result<(), Error> {
let base64 = part.to_base64()?;
self.parts.push(base64);
Ok(())
}
pub fn len(&self) -> usize {
self.parts.len()
}
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
pub fn encode(&self) -> String {
let strings: Vec<&str> = self.parts.iter().map(Deref::deref).collect();
strings.join(".")
}
pub fn decode(encoded: &str) -> Self {
let parts = encoded
.split('.')
.map(|s| FromStr::from_str(s).unwrap())
.collect();
Self { parts }
}
pub fn part<T: CompactPart>(&self, index: usize) -> Result<T, Error> {
let part = self
.parts
.get(index)
.ok_or_else(|| "Out of bounds".to_string())?;
CompactPart::from_base64(part)
}
}
impl Default for Compact {
fn default() -> Self {
Compact::new()
}
}
impl Display for Compact {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.encode())
}
}
impl Serialize for Compact {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.encode())
}
}
impl<'de> Deserialize<'de> for Compact {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct CompactVisitor;
impl<'de> de::Visitor<'de> for CompactVisitor {
type Value = Compact;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string containing a compact JOSE representation")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Compact::decode(value))
}
}
deserializer.deserialize_str(CompactVisitor)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SingleOrMultiple<T> {
Single(T),
Multiple(Vec<T>),
}
impl<T> SingleOrMultiple<T>
where
T: Clone + Debug + Eq + PartialEq + Serialize + DeserializeOwned + Send + Sync,
{
pub fn contains<Q: ?Sized>(&self, value: &Q) -> bool
where
T: Borrow<Q>,
Q: PartialEq,
{
match *self {
SingleOrMultiple::Single(ref single) => single.borrow() == value,
SingleOrMultiple::Multiple(ref vector) => {
vector.iter().map(Borrow::borrow).any(|v| v == value)
}
}
}
pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a T> + 'a> {
match *self {
SingleOrMultiple::Single(ref single) => Box::new(iter::once(single)),
SingleOrMultiple::Multiple(ref vector) => Box::new(vector.iter()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Timestamp(DateTime<Utc>);
impl Deref for Timestamp {
type Target = DateTime<Utc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<DateTime<Utc>> for Timestamp {
fn from(datetime: DateTime<Utc>) -> Self {
Timestamp(datetime)
}
}
impl From<Timestamp> for DateTime<Utc> {
fn from(ts: Timestamp) -> Self {
ts.0
}
}
impl From<i64> for Timestamp {
fn from(timestamp: i64) -> Self {
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(timestamp, 0), Utc).into()
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(self.timestamp())
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let timestamp = i64::deserialize(deserializer)?;
Ok(Timestamp(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(timestamp, 0),
Utc,
)))
}
}
#[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<String>>,
#[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 crate::Presence::*;
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_value()),
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(|v| v.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(Utc::now);
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(Utc::now);
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(Utc::now);
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(ref audience)) if audience != &expected_aud => Err(
ValidationError::InvalidAudience(self.audience.clone().unwrap()),
),
Some(SingleOrMultiple::Multiple(ref audiences))
if !audiences.contains(&expected_aud) =>
{
Err(ValidationError::InvalidAudience(
self.audience.clone().unwrap(),
))
}
_ => 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(self.issuer.clone().unwrap()))
}
_ => 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> CompactJson for ClaimsSet<T> where T: Serialize + DeserializeOwned {}
#[cfg(test)]
mod tests {
use std::str::{self, FromStr};
use chrono::{Duration, TimeZone, Utc};
use super::*;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct PrivateClaims {
company: String,
department: String,
}
impl CompactJson for PrivateClaims {}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct InvalidPrivateClaim {
sub: String,
company: String,
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
struct SingleOrMultipleStrings {
values: SingleOrMultiple<String>,
}
#[test]
fn single_string_serialization_round_trip() {
let test = SingleOrMultipleStrings {
values: SingleOrMultiple::Single("foobar".to_string()),
};
let expected_json = r#"{"values":"foobar"}"#;
let serialized = not_err!(serde_json::to_string(&test));
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = not_err!(serde_json::from_str(&serialized));
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: SingleOrMultiple::Multiple(vec![
"foo".to_string(),
"bar".to_string(),
"baz".to_string(),
]),
};
let expected_json = r#"{"values":["foo","bar","baz"]}"#;
let serialized = not_err!(serde_json::to_string(&test));
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = not_err!(serde_json::from_str(&serialized));
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: SingleOrMultiple::Single(not_err!(FromStr::from_str("foobar"))),
};
let expected_json = r#"{"values":"foobar"}"#;
let serialized = not_err!(serde_json::to_string(&test));
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = not_err!(serde_json::from_str(&serialized));
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: SingleOrMultiple::Single(not_err!(FromStr::from_str(
"https://www.examples.com/"
))),
};
let expected_json = r#"{"values":"https://www.examples.com/"}"#;
let serialized = not_err!(serde_json::to_string(&test));
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = not_err!(serde_json::from_str(&serialized));
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: SingleOrMultiple::Multiple(vec![
not_err!(FromStr::from_str("foo")),
not_err!(FromStr::from_str("https://www.example.com/")),
not_err!(FromStr::from_str("data:text/plain,Hello?World#")),
not_err!(FromStr::from_str("http://[::1]/")),
not_err!(FromStr::from_str("baz")),
]),
};
let expected_json = r#"{"values":["foo","https://www.example.com/","data:text/plain,Hello?World#","http://[::1]/","baz"]}"#;
let serialized = not_err!(serde_json::to_string(&test));
assert_eq!(expected_json, serialized);
let deserialized: SingleOrMultipleStrings = not_err!(serde_json::from_str(&serialized));
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() {
use chrono::Timelike;
let now: Timestamp = Utc::now().with_nanosecond(0).unwrap().into();
let serialized = not_err!(serde_json::to_string(&now));
let deserialized = not_err!(serde_json::from_str(&serialized));
assert_eq!(now, deserialized);
let fixed_time: Timestamp = 1000.into();
let serialized = not_err!(serde_json::to_string(&fixed_time));
assert_eq!(serialized, "1000");
let deserialized = not_err!(serde_json::from_str(&serialized));
assert_eq!(fixed_time, deserialized);
}
#[test]
fn empty_registered_claims_serialization_round_trip() {
let claim = RegisteredClaims::default();
let expected_json = "{}";
let serialized = not_err!(serde_json::to_string(&claim));
assert_eq!(expected_json, serialized);
let deserialized: RegisteredClaims = not_err!(serde_json::from_str(&serialized));
assert_eq!(deserialized, claim);
}
#[test]
fn registered_claims_serialization_round_trip() {
let claim = RegisteredClaims {
issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))),
audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str(
"htts://acme-customer.com/"
)))),
not_before: Some(1234.into()),
..Default::default()
};
let expected_json =
r#"{"iss":"https://www.acme.com/","aud":"htts://acme-customer.com/","nbf":1234}"#;
let serialized = not_err!(serde_json::to_string(&claim));
assert_eq!(expected_json, serialized);
let deserialized: RegisteredClaims = not_err!(serde_json::from_str(&serialized));
assert_eq!(deserialized, claim);
}
#[test]
fn claims_set_serialization_round_trip() {
let claim = ClaimsSet::<PrivateClaims> {
registered: RegisteredClaims {
issuer: Some(not_err!(FromStr::from_str("https://www.acme.com/"))),
subject: Some(not_err!(FromStr::from_str("John Doe"))),
audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str(
"htts://acme-customer.com/"
)))),
not_before: Some(1234.into()),
..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 = not_err!(serde_json::to_string(&claim));
assert_eq!(expected_json, serialized);
let deserialized: ClaimsSet<PrivateClaims> = not_err!(serde_json::from_str(&serialized));
assert_eq!(deserialized, claim);
}
#[test]
fn duplicate_claims_round_trip() {
let claim = ClaimsSet::<InvalidPrivateClaim> {
registered: RegisteredClaims {
issuer: Some(not_err!(FromStr::from_str("https://www.acme.com"))),
subject: Some(not_err!(FromStr::from_str("John Doe"))),
audience: Some(SingleOrMultiple::Single(not_err!(FromStr::from_str(
"htts://acme-customer.com"
)))),
not_before: Some(1234.into()),
..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::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::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::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::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::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::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::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(Utc.timestamp(0, 0)),
..Default::default()
};
let registered_claims = RegisteredClaims {
issued_at: Some(10.into()),
..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(Utc.timestamp(40, 0)),
..Default::default()
};
let registered_claims = RegisteredClaims {
issued_at: Some(10.into()),
..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(Utc.timestamp(2, 0)),
..Default::default()
};
let registered_claims = RegisteredClaims {
expiry: Some(1.into()),
..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(Utc.timestamp(0, 0)),
..Default::default()
};
let registered_claims = RegisteredClaims {
not_before: Some(1.into()),
..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(Utc::now() - Duration::days(2))),
issued_at: Some(Timestamp(Utc::now() - Duration::days(1))),
expiry: Some(Timestamp(Utc::now() + 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_value()),
..Default::default()
};
not_err!(registered_claims.validate(validation_options));
}
#[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::Single("audience".to_string());
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.into()),
not_before: Some(1.into()),
issued_at: Some(95.into()),
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(Utc.timestamp(100, 0)),
..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_value()),
audience: Validation::Validate("audience".to_string()),
issuer: Validation::Validate("issuer".to_string()),
};
not_err!(registered_claims.validate(validation_options));
}
#[test]
fn validate_times_valid_token_with_epsilon() {
let registered_claims = RegisteredClaims {
expiry: Some(99.into()),
not_before: Some(96.into()),
issued_at: Some(96.into()),
..Default::default()
};
let temporal_options = TemporalOptions {
now: Some(Utc.timestamp(100, 0)),
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_value()),
..Default::default()
};
not_err!(registered_claims.validate(validation_options));
}
#[test]
fn compact_part_round_trip() {
let test_value = PrivateClaims {
department: "Toilet Cleaning".to_string(),
company: "ACME".to_string(),
};
let base64 = not_err!(test_value.to_base64());
let expected_base64 = "eyJjb21wYW55IjoiQUNNRSIsImRlcGFydG1lbnQiOiJUb2lsZXQgQ2xlYW5pbmcifQ";
assert_eq!(base64.str(), expected_base64);
let actual_value = not_err!(PrivateClaims::from_base64(&base64));
assert_eq!(actual_value, test_value);
}
#[test]
fn compact_part_vec_u8_round_trip() {
let test_value: Vec<u8> = vec![1, 2, 3, 4, 5];
let base64 = not_err!(test_value.to_base64());
let expected_base64 = "AQIDBAU";
assert_eq!(base64.str(), expected_base64);
let actual_value = not_err!(Vec::<u8>::from_base64(&base64));
assert_eq!(actual_value, test_value);
}
#[test]
fn compact_part_base64_url_round_trip() {
let test_value = Base64Url("AQIDBAU".to_string());
let base64 = not_err!(test_value.to_base64());
let expected_base64 = "AQIDBAU";
assert_eq!(base64.str(), expected_base64);
let actual_value = not_err!(Base64Url::from_base64(&base64));
assert_eq!(actual_value, test_value);
}
}