diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/api/account.rs | 2 | ||||
-rw-r--r-- | src/api/users.rs | 6 | ||||
-rw-r--r-- | src/auth.rs | 6 | ||||
-rw-r--r-- | src/auth/claims.rs | 64 |
4 files changed, 56 insertions, 22 deletions
diff --git a/src/api/account.rs b/src/api/account.rs index 39b4dc0..d6a94b5 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -170,7 +170,7 @@ mod tests { let request = Request::builder() .uri("/logout") .method("GET") - .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?) + .header(COOKIE, HeaderValue::try_from(AccessClaims::issue(USER_ID))?) .body(Body::empty())?; let (mut parts, _) = router.oneshot(request).await?.into_parts(); diff --git a/src/api/users.rs b/src/api/users.rs index 2440e6e..45d69fe 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -128,7 +128,7 @@ mod tests { let request = Request::builder() .uri(format!("/users/{}", USER_ID)) - .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?) + .header(COOKIE, HeaderValue::try_from(AccessClaims::issue(USER_ID))?) .body(Body::empty())?; let response = router.oneshot(request).await?; @@ -155,7 +155,7 @@ mod tests { let request = Request::builder() .uri(format!("/users/{}", USER_ID)) - .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?) + .header(COOKIE, HeaderValue::try_from(AccessClaims::issue(USER_ID))?) .body(Body::empty())?; let response = router.oneshot(request).await?; @@ -175,7 +175,7 @@ mod tests { .uri(format!("/users/{}", USER_ID)) .header( COOKIE, - HeaderValue::try_from(AccessClaims::new(uuid::Uuid::new_v4()))?, + HeaderValue::try_from(AccessClaims::issue(uuid::Uuid::new_v4()))?, ) .body(Body::empty())?; diff --git a/src/auth.rs b/src/auth.rs index 3ba64ea..d2cfb3e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -37,7 +37,7 @@ pub async fn issue( Argon2::default().verify_password(basic.password().as_bytes(), &PasswordHash::new(&p)?)?; - let refresh = RefreshClaims::new(uuid); + let refresh = RefreshClaims::issue(uuid); let access = refresh.refresh(); Ok((access, refresh)) } @@ -58,7 +58,7 @@ pub async fn create( .ok_or(Error::Registration)? .id; - let refresh = RefreshClaims::new(uuid); + let refresh = RefreshClaims::issue(uuid); let access = refresh.refresh(); Ok((StatusCode::CREATED, (access, refresh))) @@ -87,7 +87,7 @@ mod tests { fn test_jwt_encode_decode() -> TestResult { setup_test_env(); - let claims = AccessClaims::new(uuid::Uuid::new_v4()); + let claims = AccessClaims::issue(uuid::Uuid::new_v4()); let token = JWT.encode(&claims)?; let decoded = JWT.decode(&token)?.claims; assert_eq!(claims, decoded); 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()) |