11use crate :: token_credentials:: cache:: TokenCache ;
22use azure_core:: {
33 auth:: { AccessToken , Secret , TokenCredential } ,
4- error:: { Error , ErrorKind } ,
4+ error:: { Error , ErrorKind , ResultExt } ,
55 from_json,
66} ;
77use serde:: Deserialize ;
88use std:: { process:: Command , str} ;
99use time:: OffsetDateTime ;
1010
11+ #[ cfg( feature = "old_azure_cli" ) ]
1112mod 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" ) ]
99100struct 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 ) ]
111146pub 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) ]
216252mod 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