Skip to content

Commit 9502776

Browse files
authored
Handle MsiTokenResponse::expires_on being an int or string. (#2390)
* Handle MsiTokenResponse::expires_on being an int or string. * Add changelog entry
1 parent 3bfc35f commit 9502776

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

sdk/identity/azure_identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
### Bugs Fixed
2525

26+
- Handle MSI token servers which respond with `expires_on: i64` instead of `expires_on: String`
27+
2628
### Other Changes
2729

2830
## 0.22.0 (2025-02-18)

sdk/identity/azure_identity/src/credentials/imds_managed_identity_credentials.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,45 @@ impl TokenCredential for ImdsManagedIdentityCredential {
148148
}
149149
}
150150

151+
// `expires_on` varies between a number and a date string depending on token server implementation
152+
// https://github.com/Azure/azure-sdk-for-go/blob/66eca06a3fb1a931ddd3c7e61462967f6e5b9c2e/sdk/azidentity/managed_identity_client.go#L310
151153
fn expires_on_string<'de, D>(deserializer: D) -> std::result::Result<OffsetDateTime, D::Error>
152154
where
153155
D: Deserializer<'de>,
154156
{
155-
let v = String::deserialize(deserializer)?;
156-
let as_i64 = v.parse::<i64>().map_err(de::Error::custom)?;
157-
OffsetDateTime::from_unix_timestamp(as_i64).map_err(de::Error::custom)
157+
struct ExpiresOnVisitor;
158+
159+
impl de::Visitor<'_> for ExpiresOnVisitor {
160+
type Value = OffsetDateTime;
161+
162+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
163+
formatter.write_str("a string or integer representing a Unix timestamp")
164+
}
165+
166+
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
167+
where
168+
E: de::Error,
169+
{
170+
let as_i64 = value.parse::<i64>().map_err(de::Error::custom)?;
171+
OffsetDateTime::from_unix_timestamp(as_i64).map_err(de::Error::custom)
172+
}
173+
174+
fn visit_i64<E>(self, value: i64) -> std::result::Result<Self::Value, E>
175+
where
176+
E: de::Error,
177+
{
178+
OffsetDateTime::from_unix_timestamp(value).map_err(de::Error::custom)
179+
}
180+
181+
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
182+
where
183+
E: de::Error,
184+
{
185+
OffsetDateTime::from_unix_timestamp(value as i64).map_err(de::Error::custom)
186+
}
187+
}
188+
189+
deserializer.deserialize_any(ExpiresOnVisitor)
158190
}
159191

160192
/// Convert a `AADv2` scope to an `AADv1` resource
@@ -179,7 +211,7 @@ fn scopes_to_resource<'a>(scopes: &'a [&'a str]) -> azure_core::Result<&'a str>
179211
Ok(scope.strip_suffix("/.default").unwrap_or(*scope))
180212
}
181213

182-
// NOTE: expires_on is a String version of unix epoch time, not an integer.
214+
// NOTE: expires_on is _meant_ to be a String version of unix epoch time, not an integer, though it varies between implementations.
183215
// https://learn.microsoft.com/azure/app-service/overview-managed-identity?tabs=dotnet#rest-protocol-examples
184216
#[derive(Debug, Clone, Deserialize)]
185217
#[allow(unused)]
@@ -210,4 +242,20 @@ mod tests {
210242
assert_eq!(expected, parsed.date);
211243
Ok(())
212244
}
245+
246+
#[test]
247+
fn check_expires_on_int() -> azure_core::Result<()> {
248+
let as_string = r#"{"date": 1586984735}"#;
249+
let expected = datetime!(2020-4-15 21:5:35 UTC);
250+
let parsed: TestExpires = from_json(as_string)?;
251+
assert_eq!(expected, parsed.date);
252+
Ok(())
253+
}
254+
255+
#[test]
256+
fn check_expires_on_invalid() {
257+
let as_string = r#"{"date": "invalid"}"#;
258+
let parsed: Result<TestExpires, Error> = from_json(as_string);
259+
assert!(parsed.is_err());
260+
}
213261
}

0 commit comments

Comments
 (0)