From c99e9b51ae544bee8dbfe909315baafd27158939 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 17 Jan 2024 14:57:31 -0600 Subject: [PATCH 1/3] AzureCliCredential utilizes expires_on token for expiration --- sdk/identity/Azure.Identity/CHANGELOG.md | 1 + .../src/Credentials/AzureCliCredential.cs | 4 ++-- .../tests/AzureCliCredentialTests.cs | 24 ++++++++++++++----- .../tests/CredentialTestHelpers.cs | 13 ++++++---- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index b2433aa4d1be3..af081adbe428f 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -10,6 +10,7 @@ - Claims from the `TokenRequestContext` are now correctly sent through to MSAL in `ConfidentialClient` credentials. [#40451](https://github.com/Azure/azure-sdk-for-net/issues/40451). ### Other Changes +- `AzureCliCredential` utilizes the new `expires_on` property returned by `az account get-access-token` to determine token expiration. ## 1.10.4 (2023-11-13) diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs index 95d4c0eb2ce8d..477958dbde6c1 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs @@ -233,8 +233,8 @@ private static AccessToken DeserializeOutput(string output) JsonElement root = document.RootElement; string accessToken = root.GetProperty("accessToken").GetString(); - DateTimeOffset expiresOn = root.TryGetProperty("expiresIn", out JsonElement expiresIn) - ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(expiresIn.GetInt64()) + DateTimeOffset expiresOn = root.TryGetProperty("expires_on", out JsonElement expires_on) + ? DateTimeOffset.FromUnixTimeSeconds(expires_on.GetInt64()) : DateTimeOffset.ParseExact(root.GetProperty("expiresOn").GetString(), "yyyy-MM-dd HH:mm:ss.ffffff", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal); return new AccessToken(accessToken, expiresOn); diff --git a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs index 50bd6f48d2392..2117aa7d69f23 100644 --- a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs @@ -72,16 +72,28 @@ public async Task AuthenticateWithCliCredential( } [Test] - public async Task AuthenticateWithCliCredential_ExpiresIn() + public async Task AuthenticateWithCliCredential_expires_on() { - var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzureCliExpiresIn(1800); + var now = DateTimeOffset.UtcNow; + DateTimeOffset expectedExpiresOn = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, TimeSpan.Zero).AddHours(1); + var (expectedToken1, processOutput1) = CredentialTestHelpers.CreateTokenForAzureCliExpiresOn(expectedExpiresOn, true); + var (expectedToken2, processOutput2) = CredentialTestHelpers.CreateTokenForAzureCliExpiresOn(expectedExpiresOn, false); - var testProcess = new TestProcess { Output = processOutput }; + var testProcess = new TestProcess { Output = processOutput1 }; AzureCliCredential credential = InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess))); - AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); + AccessToken actualToken1 = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); - Assert.AreEqual(expectedToken, actualToken.Token); - Assert.LessOrEqual(expectedExpiresOn, actualToken.ExpiresOn); + Assert.AreEqual(expectedToken1, actualToken1.Token, "The tokens should match."); + Assert.AreEqual(expectedExpiresOn, actualToken1.ExpiresOn, "The expires on value should be the same for token1."); + + testProcess = new TestProcess { Output = processOutput2 }; + credential = InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess))); + AccessToken actualToken2 = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); + + Assert.AreEqual(expectedToken2, actualToken2.Token); + Assert.AreEqual(expectedExpiresOn, actualToken2.ExpiresOn, "The expires on value should be the same for token2."); + + Assert.AreEqual(actualToken1.ExpiresOn, actualToken2.ExpiresOn); } [Test] diff --git a/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs b/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs index e74df86d0bd9f..82a376e53830a 100644 --- a/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs +++ b/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs @@ -17,7 +17,6 @@ using Azure.Identity.Tests.Mock; using Microsoft.Identity.Client; using NUnit.Framework; -using Castle.DynamicProxy; namespace Azure.Identity.Tests { @@ -39,12 +38,16 @@ public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenF return (token, expiresOn, json); } - public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenForAzureCliExpiresIn(int seconds = 30) + public static (string Token, string Json) CreateTokenForAzureCliExpiresOn(DateTimeOffset expiresOn, bool includeExpiresOn) { - var expiresOn = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + const string expiresOnStringFormat = "yyyy-MM-dd HH:mm:ss.ffffff"; + + var expiresOnString = expiresOn.ToLocalTime().ToString(expiresOnStringFormat); var token = TokenGenerator.GenerateToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), expiresOn.UtcDateTime); - var json = $"{{ \"accessToken\": \"{token}\", \"expiresIn\": {seconds} }}"; - return (token, expiresOn, json); + var json = includeExpiresOn ? + $"{{ \"accessToken\": \"{token}\", \"expiresOn\": \"{expiresOnString}\", \"expires_on\": {expiresOn.ToUnixTimeSeconds()} }}" : + $"{{ \"accessToken\": \"{token}\", \"expiresOn\": \"{expiresOnString}\" }}"; + return (token, json); } public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenForAzureDeveloperCli() => CreateTokenForAzureDeveloperCli(TimeSpan.FromSeconds(30)); From 8bae2830c79b6ce7be391f9006856afb81675fd0 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 18 Jan 2024 09:16:02 -0600 Subject: [PATCH 2/3] Update sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs Co-authored-by: Heath Stewart --- sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs b/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs index 82a376e53830a..18f9f15453004 100644 --- a/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs +++ b/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs @@ -45,7 +45,7 @@ public static (string Token, string Json) CreateTokenForAzureCliExpiresOn(DateTi var expiresOnString = expiresOn.ToLocalTime().ToString(expiresOnStringFormat); var token = TokenGenerator.GenerateToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), expiresOn.UtcDateTime); var json = includeExpiresOn ? - $"{{ \"accessToken\": \"{token}\", \"expiresOn\": \"{expiresOnString}\", \"expires_on\": {expiresOn.ToUnixTimeSeconds()} }}" : + $$"""{ "accessToken": "{{token}}", "expiresOn": "{{expiresOnString}}", "expires_on": {{expiresOn.ToUnixTimeSeconds()}} }" : $"{{ \"accessToken\": \"{token}\", \"expiresOn\": \"{expiresOnString}\" }}"; return (token, json); } From 1fb296d3c6e864b3093c75a009114b73a9eb2916 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 18 Jan 2024 09:24:32 -0600 Subject: [PATCH 3/3] string literal fix --- sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs b/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs index 18f9f15453004..5e1e912eb694a 100644 --- a/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs +++ b/sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs @@ -45,8 +45,8 @@ public static (string Token, string Json) CreateTokenForAzureCliExpiresOn(DateTi var expiresOnString = expiresOn.ToLocalTime().ToString(expiresOnStringFormat); var token = TokenGenerator.GenerateToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), expiresOn.UtcDateTime); var json = includeExpiresOn ? - $$"""{ "accessToken": "{{token}}", "expiresOn": "{{expiresOnString}}", "expires_on": {{expiresOn.ToUnixTimeSeconds()}} }" : - $"{{ \"accessToken\": \"{token}\", \"expiresOn\": \"{expiresOnString}\" }}"; + $$"""{ "accessToken": "{{token}}", "expiresOn": "{{expiresOnString}}", "expires_on": {{expiresOn.ToUnixTimeSeconds()}} }""" : + $$"""{ "accessToken": "{{token}}", "expiresOn": "{{expiresOnString}}" }"""; return (token, json); }