Skip to content

Commit

Permalink
Allow updated_at to be a string epoch
Browse files Browse the repository at this point in the history
Allow unix timestamps within a string for updated_at, as follows:
  "updated_at": "1713964430.299453"

This is not allowed by the specification, but was observed in the wild on at least one OpenAM
instance. This misbehavior is probably caused by a plugin.

This parsing leniancy feature is gated behind "accept-string-epoch".
  • Loading branch information
multun committed Apr 25, 2024
1 parent c4e28f4 commit 375812e
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" }
[features]
accept-rfc3339-timestamps = []
accept-string-booleans = []
accept-string-epoch = []
curl = ["oauth2/curl"]
default = ["reqwest", "rustls-tls"]
native-tls = ["oauth2/native-tls"]
Expand Down
61 changes: 60 additions & 1 deletion src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,59 @@ where
}
}

// Some providers return numbers as strings
#[cfg(feature = "accept-string-epoch")]
pub(crate) mod serde_string_number {
use serde::{de, Deserializer};

use std::fmt;

pub fn deserialize<'de, D>(deserializer: D) -> Result<serde_json::Number, D::Error>
where
D: Deserializer<'de>,
{
struct StringLikeNumberVisitor;

impl<'de> de::Visitor<'de> for StringLikeNumberVisitor {
type Value = serde_json::Number;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a JSON number")
}

fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.into())
}

fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.into())
}

fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
serde_json::Number::from_f64(v)
.ok_or_else(|| de::Error::custom("not a JSON number"))
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
serde_json::from_str(v).map_err(|_| de::Error::custom("not a JSON number"))
}
}
deserializer.deserialize_any(StringLikeNumberVisitor)
}
}

// Some providers return boolean values as strings. Provide support for
// parsing using stdlib.
#[cfg(feature = "accept-string-booleans")]
Expand Down Expand Up @@ -331,7 +384,13 @@ impl Display for Boolean {
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub(crate) enum Timestamp {
Seconds(serde_json::Number),
Seconds(
#[cfg_attr(
feature = "accept-string-epoch",
serde(deserialize_with = "crate::helpers::serde_string_number::deserialize")
)]
serde_json::Number,
),
#[cfg(feature = "accept-rfc3339-timestamps")]
Rfc3339(String),
}
Expand Down
32 changes: 32 additions & 0 deletions src/id_token/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,38 @@ fn test_accept_rfc3339_timestamp() {
);
}

#[test]
#[cfg(feature = "accept-string-epoch")]
fn test_accept_string_updated_at() {
for (updated_at, sec, nsec) in [
("1713963222.5", 1713963222, 500_000_000),
("42.5", 42, 500_000_000),
("42", 42, 0),
("-42", -42, 0),
] {
let payload = format!(
"{{
\"iss\": \"https://server.example.com\",
\"sub\": \"24400320\",
\"aud\": \"s6BhdRkqt3\",
\"exp\": 1311281970,
\"iat\": 1311280970,
\"updated_at\": \"{updated_at}\"
}}"
);
let claims: CoreIdTokenClaims =
serde_json::from_str(payload.as_str()).expect("failed to deserialize");
assert_eq!(
claims.updated_at(),
Some(
Utc.timestamp_opt(sec, nsec)
.single()
.expect("valid timestamp")
)
);
}
}

#[test]
fn test_unknown_claims_serde() {
let expected_serialized_claims = "{\
Expand Down

0 comments on commit 375812e

Please sign in to comment.