diff options
author | Toby Vincent <tobyv@tobyvin.dev> | 2024-04-16 12:21:37 -0500 |
---|---|---|
committer | Toby Vincent <tobyv@tobyvin.dev> | 2024-04-16 12:21:37 -0500 |
commit | 469cbc20853bcae0e74922f16f7a969d1b7a9a67 (patch) | |
tree | 6669218f0e4350a7a7050961e432bf80512b3551 /src/auth/claims.rs | |
parent | 940de575ca40e906c714c6b4ae1d1cbb6b4fc3d6 (diff) |
refactor: store claim fields as OffsetDateTime
Diffstat (limited to 'src/auth/claims.rs')
-rw-r--r-- | src/auth/claims.rs | 64 |
1 files changed, 49 insertions, 15 deletions
diff --git a/src/auth/claims.rs b/src/auth/claims.rs index bee1c35..20a4d3f 100644 --- a/src/auth/claims.rs +++ b/src/auth/claims.rs @@ -15,7 +15,7 @@ use axum_extra::{ TypedHeader, }; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use time::OffsetDateTime; +use time::{Duration, OffsetDateTime}; use uuid::Uuid; use super::{Error, JWT}; @@ -24,20 +24,31 @@ use super::{Error, JWT}; #[serde(remote = "Self")] pub struct Claims<const LIFETIME: i64 = ACCESS> { pub sub: Uuid, - pub iat: i64, - pub exp: i64, - pub jti: Uuid, + #[serde(with = "numeric_date")] + iat: OffsetDateTime, + #[serde(with = "numeric_date")] + exp: OffsetDateTime, + jti: Uuid, } impl<const LIFETIME: i64> Claims<LIFETIME> { - pub fn new(uuid: Uuid) -> Self { - let now = OffsetDateTime::now_utc().unix_timestamp(); - Self { - sub: uuid, - iat: now, - exp: now + LIFETIME, - jti: uuid::Uuid::new_v4(), - } + pub fn new(sub: Uuid, iat: OffsetDateTime) -> Self { + let iat = iat + .replace_millisecond(0) + .expect("Failed to remove millisecond from datetime. This should not have happened."); + + let exp = iat + Duration::new(LIFETIME, 0); + let jti = uuid::Uuid::new_v4(); + + Self { sub, iat, exp, jti } + } + + pub fn issue(uuid: Uuid) -> Self { + Self::new(uuid, OffsetDateTime::now_utc()) + } + + pub fn expired(&self) -> bool { + self.exp > OffsetDateTime::now_utc() } } @@ -57,7 +68,7 @@ impl<'de, const LIFETIME: i64> Deserialize<'de> for Claims<LIFETIME> { { let claims = Self::deserialize(deserializer)?; - if claims.exp - claims.iat != LIFETIME { + if claims.exp - claims.iat != Duration::new(LIFETIME, 0) { return Err(serde::de::Error::custom( "Lifetime is invalid for Claim type", )); @@ -67,6 +78,29 @@ impl<'de, const LIFETIME: i64> Deserialize<'de> for Claims<LIFETIME> { } } +mod numeric_date { + //! Custom serialization of OffsetDateTime to conform with the JWT spec (RFC 7519 section 2, "Numeric Date") + use serde::{self, Deserialize, Deserializer, Serializer}; + use time::OffsetDateTime; + + /// 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> + where + S: Serializer, + { + serializer.serialize_i64(date.unix_timestamp()) + } + + /// Attempts to deserialize an i64 and use as a Unix timestamp + pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error> + where + D: Deserializer<'de>, + { + OffsetDateTime::from_unix_timestamp(i64::deserialize(deserializer)?) + .map_err(|_| serde::de::Error::custom("invalid Unix timestamp value")) + } +} + // 1 day in seconds const ACCESS: i64 = 86400; @@ -74,7 +108,7 @@ pub type AccessClaims = Claims<ACCESS>; impl From<RefreshClaims> for AccessClaims { fn from(value: RefreshClaims) -> Self { - Claims::new(value.sub) + Claims::issue(value.sub) } } @@ -83,7 +117,7 @@ impl TryFrom<AccessClaims> for Cookie<'_> { fn try_from(value: AccessClaims) -> Result<Self, Self::Error> { Ok(Cookie::build(("token", JWT.encode(&value)?)) - .expires(OffsetDateTime::from_unix_timestamp(value.exp)?) + .expires(value.exp) .secure(true) .http_only(true) .build()) |