2021-09-28 04:04:51 -04:00
|
|
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
2019-11-03 10:46:08 -05:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-11-01 09:26:13 -04:00
|
|
|
use time::{Duration, OffsetDateTime};
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
const SECRET: &str = "some-secret";
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
struct Claims {
|
|
|
|
sub: String,
|
|
|
|
#[serde(with = "jwt_numeric_date")]
|
2021-11-01 09:26:13 -04:00
|
|
|
iat: OffsetDateTime,
|
2019-04-01 06:11:28 -04:00
|
|
|
#[serde(with = "jwt_numeric_date")]
|
2021-11-01 09:26:13 -04:00
|
|
|
exp: OffsetDateTime,
|
2019-04-01 06:11:28 -04:00
|
|
|
}
|
|
|
|
|
2020-04-24 13:57:51 -04:00
|
|
|
impl Claims {
|
|
|
|
/// If a token should always be equal to its representation after serializing and deserializing
|
2021-11-01 09:26:13 -04:00
|
|
|
/// again, this function must be used for construction. `OffsetDateTime` contains a microsecond
|
|
|
|
/// field but JWT timestamps are defined as UNIX timestamps (seconds). This function normalizes
|
|
|
|
/// the timestamps.
|
|
|
|
pub fn new(sub: String, iat: OffsetDateTime, exp: OffsetDateTime) -> Self {
|
2020-04-24 13:57:51 -04:00
|
|
|
// normalize the timestamps by stripping of microseconds
|
2021-11-01 09:26:13 -04:00
|
|
|
let iat = iat
|
|
|
|
.date()
|
|
|
|
.with_hms_milli(iat.hour(), iat.minute(), iat.second(), 0)
|
|
|
|
.unwrap()
|
|
|
|
.assume_utc();
|
|
|
|
let exp = exp
|
|
|
|
.date()
|
|
|
|
.with_hms_milli(exp.hour(), exp.minute(), exp.second(), 0)
|
|
|
|
.unwrap()
|
|
|
|
.assume_utc();
|
|
|
|
|
2020-04-24 13:57:51 -04:00
|
|
|
Self { sub, iat, exp }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-01 06:11:28 -04:00
|
|
|
mod jwt_numeric_date {
|
2021-11-01 09:26:13 -04:00
|
|
|
//! Custom serialization of OffsetDateTime to conform with the JWT spec (RFC 7519 section 2, "Numeric Date")
|
2019-04-01 06:11:28 -04:00
|
|
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
2021-11-01 09:26:13 -04:00
|
|
|
use time::OffsetDateTime;
|
2019-04-01 06:11:28 -04:00
|
|
|
|
2021-11-01 09:26:13 -04:00
|
|
|
/// Serializes an OffsetDateTime to a Unix timestamp (milliseconds since 1970/1/1T00:00:00T)
|
|
|
|
pub fn serialize<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
2019-04-01 06:11:28 -04:00
|
|
|
where
|
|
|
|
S: Serializer,
|
|
|
|
{
|
2021-11-01 09:26:13 -04:00
|
|
|
let timestamp = date.unix_timestamp();
|
2019-04-01 06:11:28 -04:00
|
|
|
serializer.serialize_i64(timestamp)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempts to deserialize an i64 and use as a Unix timestamp
|
2021-11-01 09:26:13 -04:00
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
|
2019-04-01 06:11:28 -04:00
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
2021-11-01 09:26:13 -04:00
|
|
|
OffsetDateTime::from_unix_timestamp(i64::deserialize(deserializer)?)
|
|
|
|
.map_err(|_| serde::de::Error::custom("invalid Unix timestamp value"))
|
2019-04-01 06:11:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-11-01 09:26:13 -04:00
|
|
|
const EXPECTED_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gT2Zmc2V0RGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjMyNTAzNjgwMDAwfQ.BcPipupP9oIV6uFRI6Acn7FMLws_wA3oo6CrfeFF3Gg";
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
use super::super::{Claims, SECRET};
|
2021-09-28 04:04:51 -04:00
|
|
|
use jsonwebtoken::{
|
|
|
|
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation,
|
|
|
|
};
|
2021-11-01 09:26:13 -04:00
|
|
|
use time::{Duration, OffsetDateTime};
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn round_trip() {
|
2021-11-01 09:26:13 -04:00
|
|
|
let sub = "Custom OffsetDateTime ser/de".to_string();
|
|
|
|
let iat = OffsetDateTime::from_unix_timestamp(0).unwrap();
|
|
|
|
let exp = OffsetDateTime::from_unix_timestamp(32503680000).unwrap();
|
2019-04-01 06:11:28 -04:00
|
|
|
|
2020-04-24 13:57:51 -04:00
|
|
|
let claims = Claims::new(sub.clone(), iat, exp);
|
2019-04-01 06:11:28 -04:00
|
|
|
|
2019-12-29 12:42:35 -05:00
|
|
|
let token =
|
|
|
|
encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref()))
|
|
|
|
.expect("Failed to encode claims");
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
assert_eq!(&token, EXPECTED_TOKEN);
|
|
|
|
|
2019-12-29 15:50:06 -05:00
|
|
|
let decoded = decode::<Claims>(
|
|
|
|
&token,
|
|
|
|
&DecodingKey::from_secret(SECRET.as_ref()),
|
2021-09-28 04:04:51 -04:00
|
|
|
&Validation::new(Algorithm::HS256),
|
2019-12-29 15:50:06 -05:00
|
|
|
)
|
|
|
|
.expect("Failed to decode token");
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
assert_eq!(decoded.claims, claims);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn should_fail_on_invalid_timestamp() {
|
|
|
|
// A token with the expiry of i64::MAX + 1
|
|
|
|
let overflow_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjkyMjMzNzIwMzY4NTQ3NzYwMDB9.G2PKreA27U8_xOwuIeCYXacFYeR46f9FyENIZfCrvEc";
|
|
|
|
|
2020-04-24 13:57:51 -04:00
|
|
|
let decode_result = decode::<Claims>(
|
|
|
|
&overflow_token,
|
|
|
|
&DecodingKey::from_secret(SECRET.as_ref()),
|
2021-09-28 04:04:51 -04:00
|
|
|
&Validation::new(Algorithm::HS256),
|
2020-04-24 13:57:51 -04:00
|
|
|
);
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
assert!(decode_result.is_err());
|
|
|
|
}
|
2020-04-24 13:57:51 -04:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn to_token_and_parse_equals_identity() {
|
2021-11-01 09:26:13 -04:00
|
|
|
let iat = OffsetDateTime::now_utc();
|
2020-04-24 13:57:51 -04:00
|
|
|
let exp = iat + Duration::days(1);
|
2021-11-01 09:26:13 -04:00
|
|
|
let sub = "Custom OffsetDateTime ser/de".to_string();
|
2020-04-24 13:57:51 -04:00
|
|
|
|
|
|
|
let claims = Claims::new(sub.clone(), iat, exp);
|
|
|
|
|
|
|
|
let token =
|
|
|
|
encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref()))
|
|
|
|
.expect("Failed to encode claims");
|
|
|
|
|
|
|
|
let decoded = decode::<Claims>(
|
|
|
|
&token,
|
|
|
|
&DecodingKey::from_secret(SECRET.as_ref()),
|
2021-09-28 04:04:51 -04:00
|
|
|
&Validation::new(Algorithm::HS256),
|
2020-04-24 13:57:51 -04:00
|
|
|
)
|
|
|
|
.expect("Failed to decode token")
|
|
|
|
.claims;
|
|
|
|
|
|
|
|
assert_eq!(claims, decoded);
|
|
|
|
}
|
2019-04-01 06:11:28 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
2021-11-01 09:26:13 -04:00
|
|
|
let sub = "Custom OffsetDateTime ser/de".to_string();
|
|
|
|
let iat = OffsetDateTime::now_utc();
|
|
|
|
let exp = iat + Duration::days(1);
|
2019-04-01 06:11:28 -04:00
|
|
|
|
2020-08-31 06:04:57 -04:00
|
|
|
let claims = Claims::new(sub, iat, exp);
|
2019-04-01 06:11:28 -04:00
|
|
|
|
2019-12-29 12:42:35 -05:00
|
|
|
let token = jsonwebtoken::encode(
|
|
|
|
&Header::default(),
|
|
|
|
&claims,
|
|
|
|
&EncodingKey::from_secret(SECRET.as_ref()),
|
|
|
|
)?;
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
println!("serialized token: {}", &token);
|
|
|
|
|
2019-12-29 15:50:06 -05:00
|
|
|
let token_data = jsonwebtoken::decode::<Claims>(
|
|
|
|
&token,
|
|
|
|
&DecodingKey::from_secret(SECRET.as_ref()),
|
2021-09-28 04:04:51 -04:00
|
|
|
&Validation::new(Algorithm::HS256),
|
2019-12-29 15:50:06 -05:00
|
|
|
)?;
|
2019-04-01 06:11:28 -04:00
|
|
|
|
|
|
|
println!("token data:\n{:#?}", &token_data);
|
|
|
|
Ok(())
|
|
|
|
}
|