diff --git a/src/helpers.rs b/src/helpers.rs index 76f5457..9444173 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -62,6 +62,58 @@ where } } +// Some providers return numbers as strings +pub(crate) mod serde_string_number { + use serde::{de, Deserializer}; + + use std::fmt; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + 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(self, v: i64) -> Result + where + E: de::Error, + { + Ok(v.into()) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(v.into()) + } + + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + serde_json::Number::from_f64(v) + .ok_or_else(|| de::Error::custom("not a JSON number")) + } + + fn visit_str(self, v: &str) -> Result + 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")] @@ -331,7 +383,10 @@ impl Display for Boolean { #[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] pub(crate) enum Timestamp { - Seconds(serde_json::Number), + Seconds( + #[serde(deserialize_with = "crate::helpers::serde_string_number::deserialize")] + serde_json::Number, + ), #[cfg(feature = "accept-rfc3339-timestamps")] Rfc3339(String), } diff --git a/src/id_token/tests.rs b/src/id_token/tests.rs index 304ab8c..8291bcf 100644 --- a/src/id_token/tests.rs +++ b/src/id_token/tests.rs @@ -489,6 +489,37 @@ fn test_accept_rfc3339_timestamp() { ); } +#[test] +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 = "{\