summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/api/account.rs2
-rw-r--r--src/api/users.rs6
-rw-r--r--src/auth.rs6
-rw-r--r--src/auth/claims.rs64
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())