Skip to content

Commit 207cb54

Browse files
authored
use expire_on from Azure CLI 2.54.0 if it exists (#1534)
* use expire_on from Azure CLI 2.54.0 * spelling * rename to local_expires_on & expires_on * fix test
1 parent 1160d50 commit 207cb54

File tree

2 files changed

+99
-10
lines changed

2 files changed

+99
-10
lines changed

sdk/identity/Cargo.toml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ oauth2 = { version = "4.0.0", default-features = false }
1919
url = "2.2"
2020
futures = "0.3"
2121
serde = { version = "1.0", features = ["derive"] }
22-
time = { version = "0.3.10", features = ["local-offset"] }
22+
time = { version = "0.3.10" }
2323
log = "0.4"
2424
async-trait = "0.1"
2525
openssl = { version = "0.10.46", optional=true }
2626
uuid = { version = "1.0", features = ["v4"] }
2727
pin-project = "1.0"
2828

2929
[target.'cfg(unix)'.dependencies]
30-
tz-rs = "0.6"
30+
tz-rs = { version = "0.6", optional = true }
3131

3232
[dev-dependencies]
3333
reqwest = { version = "0.11", features = ["json"], default-features = false }
@@ -38,7 +38,7 @@ azure_security_keyvault = { path = "../security_keyvault", default-features = fa
3838
serial_test = "2.0"
3939

4040
[features]
41-
default = ["development", "enable_reqwest"]
41+
default = ["development", "enable_reqwest", "old_azure_cli"]
4242
enable_reqwest = ["azure_core/enable_reqwest"]
4343
enable_reqwest_rustls = ["azure_core/enable_reqwest_rustls"]
4444
development = []
@@ -47,8 +47,15 @@ client_certificate = ["openssl"]
4747
vendored_openssl = ["openssl/vendored"]
4848
azureauth_cli = []
4949

50+
# If you are using and Azure CLI version older than 2.54.0 from November 2023,
51+
# upgrade your Azure CLI version or enable this feature.
52+
# Azure CLI 2.54.0 and above has an "expires_on" timestamp that we can use.
53+
# https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.54.0
54+
# https://github.com/Azure/azure-cli/issues/19700
55+
old_azure_cli = ["time/local-offset", "tz-rs"]
56+
5057
[package.metadata.docs.rs]
51-
features = ["enable_reqwest", "enable_reqwest_rustls", "development", "client_certificate", "azureauth_cli"]
58+
features = ["enable_reqwest", "enable_reqwest_rustls", "development", "client_certificate", "azureauth_cli", "old_azure_cli"]
5259

5360
[[example]]
5461
name="client_certificate_credentials"

sdk/identity/src/token_credentials/azure_cli_credentials.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use crate::token_credentials::cache::TokenCache;
22
use azure_core::{
33
auth::{AccessToken, Secret, TokenCredential},
4-
error::{Error, ErrorKind},
4+
error::{Error, ErrorKind, ResultExt},
55
from_json,
66
};
77
use serde::Deserialize;
88
use std::{process::Command, str};
99
use time::OffsetDateTime;
1010

11+
#[cfg(feature = "old_azure_cli")]
1112
mod az_cli_date_format {
1213
use azure_core::error::{ErrorKind, ResultExt};
1314
use serde::{self, Deserialize, Deserializer};
@@ -94,18 +95,52 @@ mod az_cli_date_format {
9495
}
9596
}
9697

98+
/// The response from `az account get-access-token --output json`.
9799
#[derive(Debug, Clone, Deserialize)]
98-
#[serde(rename_all = "camelCase")]
99100
struct CliTokenResponse {
101+
#[serde(rename = "accessToken")]
100102
pub access_token: Secret,
101-
#[serde(with = "az_cli_date_format")]
102-
pub expires_on: OffsetDateTime,
103+
#[cfg(feature = "old_azure_cli")]
104+
#[serde(rename = "expiresOn", with = "az_cli_date_format")]
105+
/// The token's expiry time formatted in the local timezone.
106+
/// Unfortunately, this requires additional timezone dependencies.
107+
/// See https://github.com/Azure/azure-cli/issues/19700 for details.
108+
pub local_expires_on: OffsetDateTime,
109+
#[serde(rename = "expires_on")]
110+
/// The token's expiry time in seconds since the epoch, a unix timestamp.
111+
/// Available in Azure CLI 2.54.0 or newer.
112+
pub expires_on: Option<i64>,
103113
pub subscription: String,
104114
pub tenant: String,
105115
#[allow(unused)]
116+
#[serde(rename = "tokenType")]
106117
pub token_type: String,
107118
}
108119

120+
impl CliTokenResponse {
121+
pub fn expires_on(&self) -> azure_core::Result<OffsetDateTime> {
122+
match self.expires_on {
123+
Some(timestamp) => Ok(OffsetDateTime::from_unix_timestamp(timestamp)
124+
.with_context(ErrorKind::DataConversion, || {
125+
format!("unable to parse expires_on '{timestamp}'")
126+
})?),
127+
None => {
128+
#[cfg(feature = "old_azure_cli")]
129+
{
130+
Ok(self.local_expires_on)
131+
}
132+
#[cfg(not(feature = "old_azure_cli"))]
133+
{
134+
Err(Error::message(
135+
ErrorKind::DataConversion,
136+
"expires_on field not found. Please use Azure CLI 2.54.0 or newer.",
137+
))
138+
}
139+
}
140+
}
141+
}
142+
}
143+
109144
/// Enables authentication to Azure Active Directory using Azure CLI to obtain an access token.
110145
#[derive(Debug)]
111146
pub struct AzureCliCredential {
@@ -195,7 +230,8 @@ impl AzureCliCredential {
195230

196231
async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
197232
let tr = Self::get_access_token(Some(scopes))?;
198-
Ok(AccessToken::new(tr.access_token, tr.expires_on))
233+
let expires_on = tr.expires_on()?;
234+
Ok(AccessToken::new(tr.access_token, expires_on))
199235
}
200236
}
201237

@@ -215,9 +251,12 @@ impl TokenCredential for AzureCliCredential {
215251
#[cfg(test)]
216252
mod tests {
217253
use super::*;
254+
#[cfg(feature = "old_azure_cli")]
218255
use serial_test::serial;
256+
#[cfg(feature = "old_azure_cli")]
219257
use time::macros::datetime;
220258

259+
#[cfg(feature = "old_azure_cli")]
221260
#[test]
222261
#[serial]
223262
fn can_parse_expires_on() -> azure_core::Result<()> {
@@ -230,7 +269,7 @@ mod tests {
230269
Ok(())
231270
}
232271

233-
#[cfg(unix)]
272+
#[cfg(all(feature = "old_azure_cli", unix))]
234273
#[test]
235274
#[serial]
236275
/// test the timezone conversion works as expected on unix platforms
@@ -255,4 +294,47 @@ mod tests {
255294

256295
Ok(())
257296
}
297+
298+
/// Test from_json for CliTokenResponse for old Azure CLI
299+
#[test]
300+
fn read_old_cli_token_response() -> azure_core::Result<()> {
301+
let json = br#"
302+
{
303+
"accessToken": "MuchLonger_NotTheRealOne_Sv8Orn0Wq0OaXuQEg",
304+
"expiresOn": "2024-01-01 19:23:16.000000",
305+
"subscription": "33b83be5-faf7-42ea-a712-320a5f9dd111",
306+
"tenant": "065e9f5e-870d-4ed1-af2b-1b58092353f3",
307+
"tokenType": "Bearer"
308+
}
309+
"#;
310+
let token_response: CliTokenResponse = from_json(json)?;
311+
assert_eq!(
312+
token_response.tenant,
313+
"065e9f5e-870d-4ed1-af2b-1b58092353f3"
314+
);
315+
Ok(())
316+
}
317+
318+
/// Test from_json for CliTokenResponse for current Azure CLI
319+
#[test]
320+
fn read_cli_token_response() -> azure_core::Result<()> {
321+
let json = br#"
322+
{
323+
"accessToken": "MuchLonger_NotTheRealOne_Sv8Orn0Wq0OaXuQEg",
324+
"expiresOn": "2024-01-01 19:23:16.000000",
325+
"expires_on": 1704158596,
326+
"subscription": "33b83be5-faf7-42ea-a712-320a5f9dd111",
327+
"tenant": "065e9f5e-870d-4ed1-af2b-1b58092353f3",
328+
"tokenType": "Bearer"
329+
}
330+
"#;
331+
let token_response: CliTokenResponse = from_json(json)?;
332+
assert_eq!(
333+
token_response.tenant,
334+
"065e9f5e-870d-4ed1-af2b-1b58092353f3"
335+
);
336+
assert_eq!(token_response.expires_on, Some(1704158596));
337+
assert_eq!(token_response.expires_on()?.unix_timestamp(), 1704158596);
338+
Ok(())
339+
}
258340
}

0 commit comments

Comments
 (0)