The time crate is the underlying crate which chrono uses for its various operations. Unfortunately, chrono is unmaintained and older versions of the time crate have security vulnerabilites[0] which are unfixed in chrono[1]. The medallion code does not use any chrono-specific features and all uses of it could be trivially replaced with the underlying time structs. Note that this change adds calls to `expect`. Where these calls are made, the previous chrono functions also panicked internally if out-of-range values were passed. We noticed this issue while doing a similar refactoring in a program that also uses medallion[2]. [0]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html [1]: https://rustsec.org/advisories/RUSTSEC-2020-0159.html [2]: https://cl.tvl.fyi/c/depot/+/5311
261 lines
8.6 KiB
Rust
261 lines
8.6 KiB
Rust
use super::Result;
|
|
use anyhow::format_err;
|
|
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
use serde_json::value::Value;
|
|
use time::{OffsetDateTime};
|
|
|
|
/// A default claim set, including the standard, or registered, claims and the ability to specify
|
|
/// your own as custom claims.
|
|
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
|
|
pub struct Payload<T = ()> {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub iss: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub sub: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub aud: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub exp: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub nbf: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub iat: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub jti: Option<String>,
|
|
#[serde(skip_serializing)]
|
|
pub claims: Option<T>,
|
|
}
|
|
|
|
/// A convenient type alias that assumes the standard claims are sufficient, the empty tuple type
|
|
/// satisfies Claims' generic parameter as simply and clearly as possible.
|
|
pub type DefaultPayload = Payload<()>;
|
|
|
|
impl<T: Serialize + DeserializeOwned> Payload<T> {
|
|
/// This implementation simply parses the base64 data twice, first parsing out the standard
|
|
/// claims then any custom claims, assigning the latter into a copy of the former before
|
|
/// returning registered and custom claims.
|
|
pub fn from_base64(raw: &str) -> Result<Payload<T>> {
|
|
let data = decode_config(raw, URL_SAFE_NO_PAD)?;
|
|
|
|
let claims: Payload<T> = serde_json::from_slice(&data)?;
|
|
|
|
let custom: Option<T> = serde_json::from_slice(&data).ok();
|
|
|
|
Ok(Payload {
|
|
iss: claims.iss,
|
|
sub: claims.sub,
|
|
aud: claims.aud,
|
|
exp: claims.exp,
|
|
nbf: claims.nbf,
|
|
iat: claims.iat,
|
|
jti: claims.jti,
|
|
claims: custom,
|
|
})
|
|
}
|
|
|
|
/// Renders both the standard and custom claims into a single consolidated JSON representation
|
|
/// before encoding.
|
|
pub fn to_base64(&self) -> Result<String> {
|
|
if let Value::Object(mut claims_map) = serde_json::to_value(&self)? {
|
|
match self.claims {
|
|
Some(ref custom) => {
|
|
if let Value::Object(custom_map) = serde_json::to_value(&custom)? {
|
|
claims_map.extend(custom_map);
|
|
let s = serde_json::to_string(&claims_map)?;
|
|
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
|
|
Ok(enc)
|
|
} else {
|
|
Err(format_err!("Could not access custom claims."))
|
|
}
|
|
}
|
|
None => {
|
|
let s = serde_json::to_string(&claims_map)?;
|
|
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
|
|
Ok(enc)
|
|
}
|
|
}
|
|
} else {
|
|
Err(format_err!("Could not access standard claims.",))
|
|
}
|
|
}
|
|
|
|
pub fn verify(&self) -> bool {
|
|
let now = OffsetDateTime::now_utc();
|
|
let nbf_verified = match self.nbf {
|
|
Some(nbf_sec) => {
|
|
let nbf = OffsetDateTime::from_unix_timestamp(nbf_sec as i64)
|
|
.expect("nbf timestamp out of range");
|
|
nbf < now
|
|
}
|
|
None => true,
|
|
};
|
|
let exp_verified = match self.exp {
|
|
Some(exp_sec) => {
|
|
let exp = OffsetDateTime::from_unix_timestamp(exp_sec as i64)
|
|
.expect("exp timestamp out of range");
|
|
now < exp
|
|
}
|
|
None => true,
|
|
};
|
|
nbf_verified && exp_verified
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{DefaultPayload, Payload};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::default::Default;
|
|
use time::{Duration, OffsetDateTime};
|
|
|
|
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
|
struct CustomClaims {
|
|
user_id: String,
|
|
is_admin: bool,
|
|
first_name: Option<String>,
|
|
last_name: Option<String>,
|
|
}
|
|
|
|
#[test]
|
|
fn from_base64() {
|
|
let enc = "eyJhdWQiOiJsb2dpbl9zZXJ2aWNlIiwiZXhwIjoxMzAyMzE5MTAwLCJpYXQiOjEzMDIzMTcxMDAsImlzcyI6ImV4YW1wbGUuY29tIiwibmJmIjoxMzAyMzE3MTAwLCJzdWIiOiJSYW5kb20gVXNlciJ9";
|
|
let payload: DefaultPayload = Payload::from_base64(enc).unwrap();
|
|
|
|
assert_eq!(payload, create_default());
|
|
}
|
|
|
|
#[test]
|
|
fn custom_from_base64() {
|
|
let enc = "eyJleHAiOjEzMDIzMTkxMDAsImZpcnN0X25hbWUiOiJSYW5kb20iLCJpYXQiOjEzMDIzMTcxMDAsImlzX2FkbWluIjpmYWxzZSwiaXNzIjoiZXhhbXBsZS5jb20iLCJsYXN0X25hbWUiOiJVc2VyIiwidXNlcl9pZCI6IjEyMzQ1NiJ9";
|
|
let payload: Payload<CustomClaims> = Payload::from_base64(enc).unwrap();
|
|
|
|
assert_eq!(payload, create_custom());
|
|
}
|
|
|
|
#[test]
|
|
fn to_base64() {
|
|
let enc = "eyJhdWQiOiJsb2dpbl9zZXJ2aWNlIiwiZXhwIjoxMzAyMzE5MTAwLCJpYXQiOjEzMDIzMTcxMDAsImlzcyI6ImV4YW1wbGUuY29tIiwibmJmIjoxMzAyMzE3MTAwLCJzdWIiOiJSYW5kb20gVXNlciJ9";
|
|
let payload = create_default();
|
|
|
|
assert_eq!(enc, payload.to_base64().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn custom_to_base64() {
|
|
let enc = "eyJleHAiOjEzMDIzMTkxMDAsImZpcnN0X25hbWUiOiJSYW5kb20iLCJpYXQiOjEzMDIzMTcxMDAsImlzX2FkbWluIjpmYWxzZSwiaXNzIjoiZXhhbXBsZS5jb20iLCJsYXN0X25hbWUiOiJVc2VyIiwidXNlcl9pZCI6IjEyMzQ1NiJ9";
|
|
let payload = create_custom();
|
|
|
|
assert_eq!(enc, payload.to_base64().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip() {
|
|
let payload = create_default();
|
|
let enc = payload.to_base64().unwrap();
|
|
assert_eq!(payload, Payload::from_base64(&*enc).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_custom() {
|
|
let payload = create_custom();
|
|
let enc = payload.to_base64().unwrap();
|
|
assert_eq!(
|
|
payload,
|
|
Payload::<CustomClaims>::from_base64(&*enc).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn verify_nbf() {
|
|
let payload = create_with_nbf(5);
|
|
assert!(payload.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn fail_nbf() {
|
|
let payload = create_with_nbf(-5);
|
|
assert_eq!(false, payload.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn verify_exp() {
|
|
let payload = create_with_exp(5);
|
|
assert!(payload.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn fail_exp() {
|
|
let payload = create_with_exp(-5);
|
|
assert_eq!(false, payload.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn verify_nbf_exp() {
|
|
let payload = create_with_nbf_exp(5, 5);
|
|
assert!(payload.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn fail_nbf_exp() {
|
|
let payload = create_with_nbf_exp(-5, -5);
|
|
assert_eq!(false, payload.verify());
|
|
let payload = create_with_nbf_exp(5, -5);
|
|
assert_eq!(false, payload.verify());
|
|
let payload = create_with_nbf_exp(-5, 5);
|
|
assert_eq!(false, payload.verify());
|
|
}
|
|
|
|
fn create_with_nbf(offset: i64) -> DefaultPayload {
|
|
let nbf = (OffsetDateTime::now_utc() - Duration::minutes(offset)).unix_timestamp();
|
|
DefaultPayload {
|
|
nbf: Some(nbf as u64),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn create_with_exp(offset: i64) -> DefaultPayload {
|
|
let exp = (OffsetDateTime::now_utc() + Duration::minutes(offset)).unix_timestamp();
|
|
DefaultPayload {
|
|
exp: Some(exp as u64),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn create_with_nbf_exp(nbf_offset: i64, exp_offset: i64) -> DefaultPayload {
|
|
let nbf = (OffsetDateTime::now_utc() - Duration::minutes(nbf_offset)).unix_timestamp();
|
|
let exp = (OffsetDateTime::now_utc() + Duration::minutes(exp_offset)).unix_timestamp();
|
|
DefaultPayload {
|
|
nbf: Some(nbf as u64),
|
|
exp: Some(exp as u64),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn create_default() -> DefaultPayload {
|
|
DefaultPayload {
|
|
aud: Some("login_service".into()),
|
|
iat: Some(1_302_317_100),
|
|
iss: Some("example.com".into()),
|
|
exp: Some(1_302_319_100),
|
|
nbf: Some(1_302_317_100),
|
|
sub: Some("Random User".into()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn create_custom() -> Payload<CustomClaims> {
|
|
Payload {
|
|
iss: Some("example.com".into()),
|
|
iat: Some(1_302_317_100),
|
|
exp: Some(1_302_319_100),
|
|
claims: Some(CustomClaims {
|
|
user_id: "123456".into(),
|
|
is_admin: false,
|
|
first_name: Some("Random".into()),
|
|
last_name: Some("User".into()),
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|