Skip to main content

axum_security_oidc/
claims.rs

1use serde::{Deserialize, Deserializer, de};
2
3use crate::error::ClaimsError;
4
5/// A UTC timestamp represented as seconds since the Unix epoch.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub struct UtcTimestamp(i64);
8
9impl<'de> serde::Deserialize<'de> for UtcTimestamp {
10    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
11        let value = i64::deserialize(deserializer)?;
12
13        #[cfg(feature = "jiff")]
14        jiff::Timestamp::from_second(value).map_err(de::Error::custom)?;
15
16        #[cfg(feature = "chrono")]
17        if chrono::DateTime::<chrono::Utc>::from_timestamp(value, 0).is_none() {
18            return Err(de::Error::custom("timestamp out of range for chrono"));
19        }
20
21        #[cfg(feature = "time")]
22        time::OffsetDateTime::from_unix_timestamp(value).map_err(de::Error::custom)?;
23
24        Ok(UtcTimestamp(value))
25    }
26}
27
28impl UtcTimestamp {
29    pub fn as_secs(&self) -> i64 {
30        self.0
31    }
32
33    #[cfg(feature = "jiff")]
34    pub fn to_jiff(&self) -> jiff::Timestamp {
35        jiff::Timestamp::from_second(self.0).expect("validated during deserialization")
36    }
37
38    #[cfg(feature = "chrono")]
39    pub fn to_chrono(&self) -> chrono::DateTime<chrono::Utc> {
40        chrono::DateTime::from_timestamp(self.0, 0).expect("validated during deserialization")
41    }
42
43    #[cfg(feature = "time")]
44    pub fn to_time(&self) -> time::OffsetDateTime {
45        time::OffsetDateTime::from_unix_timestamp(self.0).expect("validated during deserialization")
46    }
47}
48
49/// The `address` claim from an OpenID Connect ID token.
50#[derive(Debug, Clone, Deserialize)]
51pub struct OidcAddress<'a> {
52    pub formatted: Option<&'a str>,
53    pub street_address: Option<&'a str>,
54    pub locality: Option<&'a str>,
55    pub region: Option<&'a str>,
56    pub postal_code: Option<&'a str>,
57    pub country: Option<&'a str>,
58}
59
60/// Claims from a verified OpenID Connect ID token.
61///
62/// Borrows from the token payload, so it lives no longer than the buffer it
63/// was decoded from (see [`VerifiedIdToken::claims`](crate::VerifiedIdToken::claims)).
64///
65/// Standard claims are exposed as public fields. Datetime fields are private
66/// and accessed through methods that return [`UtcTimestamp`], which can be
67/// converted to `jiff`, `chrono`, or `time` types via feature-gated methods.
68///
69/// Any non-standard claims are captured in the `extra` field via `#[serde(flatten)]`.
70#[derive(Debug, Clone, Deserialize)]
71pub struct OidcClaims<'a> {
72    // Required claims
73    #[serde(rename = "iss")]
74    pub issuer: &'a str,
75    #[serde(rename = "aud", deserialize_with = "deserialize_audience")]
76    pub audiences: Vec<&'a str>,
77    #[serde(rename = "sub")]
78    pub subject: &'a str,
79
80    // Datetime claims (private)
81    exp: UtcTimestamp,
82    iat: UtcTimestamp,
83    auth_time: Option<UtcTimestamp>,
84    updated_at: Option<UtcTimestamp>,
85
86    // Optional standard claims
87    pub nonce: Option<&'a str>,
88    pub acr: Option<&'a str>,
89    pub amr: Option<Vec<&'a str>>,
90    pub azp: Option<&'a str>,
91    pub at_hash: Option<&'a str>,
92
93    // Profile claims
94    pub name: Option<&'a str>,
95    pub given_name: Option<&'a str>,
96    pub family_name: Option<&'a str>,
97    pub middle_name: Option<&'a str>,
98    pub nickname: Option<&'a str>,
99    pub preferred_username: Option<&'a str>,
100    pub profile: Option<&'a str>,
101    pub picture: Option<&'a str>,
102    pub website: Option<&'a str>,
103    pub gender: Option<&'a str>,
104    pub birthdate: Option<&'a str>,
105    pub zoneinfo: Option<&'a str>,
106    pub locale: Option<&'a str>,
107
108    // Email claims
109    pub email: Option<&'a str>,
110    pub email_verified: Option<bool>,
111
112    // Phone claims
113    pub phone_number: Option<&'a str>,
114    pub phone_number_verified: Option<bool>,
115
116    // Address claim
117    pub address: Option<OidcAddress<'a>>,
118
119    // Any additional/custom claims
120    #[serde(flatten)]
121    pub extra: serde_json::Map<String, serde_json::Value>,
122}
123
124impl<'a> OidcClaims<'a> {
125    /// Deserialize the standard claim set from a decoded ID token payload (the
126    /// raw JSON bytes of the JWT's second segment).
127    ///
128    /// The returned claims borrow from `payload`.
129    pub fn from_payload(payload: &'a [u8]) -> Result<OidcClaims<'a>, ClaimsError> {
130        serde_json::from_slice(payload).map_err(ClaimsError::Deserialize)
131    }
132
133    pub fn expiration_secs(&self) -> i64 {
134        self.exp.as_secs()
135    }
136
137    #[cfg(feature = "jiff")]
138    pub fn expiration_jiff(&self) -> jiff::Timestamp {
139        self.exp.to_jiff()
140    }
141
142    #[cfg(feature = "chrono")]
143    pub fn expiration_chrono(&self) -> chrono::DateTime<chrono::Utc> {
144        self.exp.to_chrono()
145    }
146
147    #[cfg(feature = "time")]
148    pub fn expiration_time(&self) -> time::OffsetDateTime {
149        self.exp.to_time()
150    }
151
152    pub fn issued_at(&self) -> &UtcTimestamp {
153        &self.iat
154    }
155
156    pub fn auth_time(&self) -> Option<&UtcTimestamp> {
157        self.auth_time.as_ref()
158    }
159
160    pub fn updated_at(&self) -> Option<&UtcTimestamp> {
161        self.updated_at.as_ref()
162    }
163}
164
165/// Deserializes the `aud` claim which can be either a single string or an array of strings.
166fn deserialize_audience<'de, D>(deserializer: D) -> Result<Vec<&'de str>, D::Error>
167where
168    D: Deserializer<'de>,
169{
170    struct AudienceVisitor;
171
172    impl<'de> de::Visitor<'de> for AudienceVisitor {
173        type Value = Vec<&'de str>;
174
175        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
176            f.write_str("a string or array of strings")
177        }
178
179        fn visit_borrowed_str<E: de::Error>(self, v: &'de str) -> Result<Self::Value, E> {
180            Ok(vec![v])
181        }
182
183        fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
184            let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(1));
185            while let Some(v) = seq.next_element()? {
186                values.push(v);
187            }
188            Ok(values)
189        }
190    }
191
192    deserializer.deserialize_any(AudienceVisitor)
193}