Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions sdk/identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ oauth2 = { version = "4.0.0", default-features = false }
url = "2.2"
futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
time = { version = "0.3.10", features = ["local-offset"] }
time = { version = "0.3.10" }
log = "0.4"
async-trait = "0.1"
openssl = { version = "0.10.46", optional=true }
uuid = { version = "1.0", features = ["v4"] }
pin-project = "1.0"

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

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

[features]
default = ["development", "enable_reqwest"]
default = ["development", "enable_reqwest", "old_azure_cli"]
enable_reqwest = ["azure_core/enable_reqwest"]
enable_reqwest_rustls = ["azure_core/enable_reqwest_rustls"]
development = []
Expand All @@ -47,8 +47,15 @@ client_certificate = ["openssl"]
vendored_openssl = ["openssl/vendored"]
azureauth_cli = []

# If you are using and Azure CLI version older than 2.54.0 from November 2023,
# upgrade your Azure CLI version or enable this feature.
# Azure CLI 2.54.0 and above has an "expires_on" timestamp that we can use.
# https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.54.0
# https://github.com/Azure/azure-cli/issues/19700
old_azure_cli = ["time/local-offset", "tz-rs"]

[package.metadata.docs.rs]
features = ["enable_reqwest", "enable_reqwest_rustls", "development", "client_certificate", "azureauth_cli"]
features = ["enable_reqwest", "enable_reqwest_rustls", "development", "client_certificate", "azureauth_cli", "old_azure_cli"]

[[example]]
name="client_certificate_credentials"
Expand Down
94 changes: 88 additions & 6 deletions sdk/identity/src/token_credentials/azure_cli_credentials.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::token_credentials::cache::TokenCache;
use azure_core::{
auth::{AccessToken, Secret, TokenCredential},
error::{Error, ErrorKind},
error::{Error, ErrorKind, ResultExt},
from_json,
};
use serde::Deserialize;
use std::{process::Command, str};
use time::OffsetDateTime;

#[cfg(feature = "old_azure_cli")]
mod az_cli_date_format {
use azure_core::error::{ErrorKind, ResultExt};
use serde::{self, Deserialize, Deserializer};
Expand Down Expand Up @@ -94,18 +95,52 @@ mod az_cli_date_format {
}
}

/// The response from `az account get-access-token --output json`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CliTokenResponse {
#[serde(rename = "accessToken")]
pub access_token: Secret,
#[serde(with = "az_cli_date_format")]
pub expires_on: OffsetDateTime,
#[cfg(feature = "old_azure_cli")]
#[serde(rename = "expiresOn", with = "az_cli_date_format")]
/// The token's expiry time formatted in the local timezone.
/// Unfortunately, this requires additional timezone dependencies.
/// See https://github.com/Azure/azure-cli/issues/19700 for details.
pub local_expires_on: OffsetDateTime,
#[serde(rename = "expires_on")]
/// The token's expiry time in seconds since the epoch, a unix timestamp.
/// Available in Azure CLI 2.54.0 or newer.
pub expires_on: Option<i64>,
pub subscription: String,
pub tenant: String,
#[allow(unused)]
#[serde(rename = "tokenType")]
pub token_type: String,
}

impl CliTokenResponse {
pub fn expires_on(&self) -> azure_core::Result<OffsetDateTime> {
match self.expires_on {
Some(timestamp) => Ok(OffsetDateTime::from_unix_timestamp(timestamp)
.with_context(ErrorKind::DataConversion, || {
format!("unable to parse expires_on '{timestamp}'")
})?),
None => {
#[cfg(feature = "old_azure_cli")]
{
Ok(self.local_expires_on)
}
#[cfg(not(feature = "old_azure_cli"))]
{
Err(Error::message(
ErrorKind::DataConversion,
"expires_on field not found. Please use Azure CLI 2.54.0 or newer.",
))
}
}
}
}
}

/// Enables authentication to Azure Active Directory using Azure CLI to obtain an access token.
#[derive(Debug)]
pub struct AzureCliCredential {
Expand Down Expand Up @@ -195,7 +230,8 @@ impl AzureCliCredential {

async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
let tr = Self::get_access_token(Some(scopes))?;
Ok(AccessToken::new(tr.access_token, tr.expires_on))
let expires_on = tr.expires_on()?;
Ok(AccessToken::new(tr.access_token, expires_on))
}
}

Expand All @@ -215,9 +251,12 @@ impl TokenCredential for AzureCliCredential {
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "old_azure_cli")]
use serial_test::serial;
#[cfg(feature = "old_azure_cli")]
use time::macros::datetime;

#[cfg(feature = "old_azure_cli")]
#[test]
#[serial]
fn can_parse_expires_on() -> azure_core::Result<()> {
Expand All @@ -230,7 +269,7 @@ mod tests {
Ok(())
}

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

Ok(())
}

/// Test from_json for CliTokenResponse for old Azure CLI
#[test]
fn read_old_cli_token_response() -> azure_core::Result<()> {
let json = br#"
{
"accessToken": "MuchLonger_NotTheRealOne_Sv8Orn0Wq0OaXuQEg",
"expiresOn": "2024-01-01 19:23:16.000000",
"subscription": "33b83be5-faf7-42ea-a712-320a5f9dd111",
"tenant": "065e9f5e-870d-4ed1-af2b-1b58092353f3",
"tokenType": "Bearer"
}
"#;
let token_response: CliTokenResponse = from_json(json)?;
assert_eq!(
token_response.tenant,
"065e9f5e-870d-4ed1-af2b-1b58092353f3"
);
Ok(())
}

/// Test from_json for CliTokenResponse for current Azure CLI
#[test]
fn read_cli_token_response() -> azure_core::Result<()> {
let json = br#"
{
"accessToken": "MuchLonger_NotTheRealOne_Sv8Orn0Wq0OaXuQEg",
"expiresOn": "2024-01-01 19:23:16.000000",
"expires_on": 1704158596,
"subscription": "33b83be5-faf7-42ea-a712-320a5f9dd111",
"tenant": "065e9f5e-870d-4ed1-af2b-1b58092353f3",
"tokenType": "Bearer"
}
"#;
let token_response: CliTokenResponse = from_json(json)?;
assert_eq!(
token_response.tenant,
"065e9f5e-870d-4ed1-af2b-1b58092353f3"
);
assert_eq!(token_response.expires_on, Some(1704158596));
assert_eq!(token_response.expires_on()?.unix_timestamp(), 1704158596);
Ok(())
}
}