From ef579b270b8dee028fed40d1c193688276ee2124 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson Date: Tue, 27 May 2025 12:15:26 -0700 Subject: [PATCH 1/2] init --- Microsoft.Identity.Web.sln | 7 + docs/design/capab1.png | Bin 0 -> 82857 bytes .../managed_identity_capabilities_devex.md | 117 +++++++ .../IManagedIdentityTestHttpClientFactory.cs | 15 + .../net462/InternalAPI.Unshipped.txt | 4 +- .../net472/InternalAPI.Unshipped.txt | 4 +- .../net6.0/InternalAPI.Unshipped.txt | 4 +- .../net7.0/InternalAPI.Unshipped.txt | 4 +- .../net8.0/InternalAPI.Unshipped.txt | 4 +- .../net9.0/InternalAPI.Unshipped.txt | 4 +- .../netstandard2.0/InternalAPI.Unshipped.txt | 4 +- .../TokenAcquisition.ManagedIdentity.cs | 28 +- .../TokenAcquisition.cs | 11 +- .../daemon-app/daemon-app-msi/Program.cs | 50 +++ .../daemon-app-msi/appsettings.json | 29 ++ .../daemon-app-msi/daemon-app-msi.csproj | 29 ++ .../daemon-app/daemon-app-msi/readme.md | 48 +++ .../Mocks/MockHttpCreator.cs | 28 ++ tests/Microsoft.Identity.Web.Test/FmiTests.cs | 1 + .../ManagedIdentityCaeTests.cs | 288 ++++++++++++++++++ .../TestManagedIdentityHttpFactory.cs | 15 + .../TokenAcquirerCollection.cs | 23 ++ 22 files changed, 705 insertions(+), 12 deletions(-) create mode 100644 docs/design/capab1.png create mode 100644 docs/design/managed_identity_capabilities_devex.md create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs create mode 100644 tests/DevApps/daemon-app/daemon-app-msi/Program.cs create mode 100644 tests/DevApps/daemon-app/daemon-app-msi/appsettings.json create mode 100644 tests/DevApps/daemon-app/daemon-app-msi/daemon-app-msi.csproj create mode 100644 tests/DevApps/daemon-app/daemon-app-msi/readme.md create mode 100644 tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs create mode 100644 tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs create mode 100644 tests/Microsoft.Identity.Web.Test/TokenAcquirerCollection.cs diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln index 05c15260d..2c07d42a0 100644 --- a/Microsoft.Identity.Web.sln +++ b/Microsoft.Identity.Web.sln @@ -164,6 +164,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Oidc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.UI.Test", "tests\Microsoft.Identity.Web.UI.Test\Microsoft.Identity.Web.UI.Test.csproj", "{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "daemon-app-msi", "tests\DevApps\daemon-app\daemon-app-msi\daemon-app-msi.csproj", "{A8181404-23E0-D38B-454C-D16ECDB18B9F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -387,6 +389,10 @@ Global {CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.Build.0 = Release|Any CPU + {A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -461,6 +467,7 @@ Global {E927D215-A96C-626C-9A1A-CF99876FE7B4} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C} {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F} {CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8} = {B4E72F1C-603F-437C-AAA1-153A604CD34A} + {A8181404-23E0-D38B-454C-D16ECDB18B9F} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187} diff --git a/docs/design/capab1.png b/docs/design/capab1.png new file mode 100644 index 0000000000000000000000000000000000000000..7e6737ee9b7150f800abec53c1caeabb1f1ac5b2 GIT binary patch literal 82857 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" + } + } + } +} +``` + +> **Note** : The same configuration block works in *appsettings.json* or can be supplied programmatically. + + +## Code Snippets + +### Registering & Calling a Downstream API + +```csharp +// 1 – set up the TokenAcquirerFactory (test-helper shown for brevity) +var factory = TokenAcquirerFactory.GetDefaultInstance(); + +// 2 – register the downstream API using section "ContosoStorage" +factory.Services.AddDownstreamApi("ContosoStorage", + factory.Configuration.GetSection("ContosoStorage")); + +IServiceProvider sp = factory.Build(); +IDownstreamApi api = sp.GetRequiredService(); + +// 3 – call the API (Id.Web handles CAE automatically) +HttpResponseMessage resp = await api.CallApiForAppAsync("ContosoStorage"); +``` + +### Using **IAuthorizationHeaderProvider** (advanced) + +`IAuthorizationHeaderProvider` is fully supported with Managed Identity. Claims challenges propagate the same way: + +```csharp +var headerProvider = sp.GetRequiredService(); +string header = await headerProvider.CreateAuthorizationHeaderForAppAsync( + scope: "https://storage.contoso.com/.default", + options: new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + ManagedIdentity = new ManagedIdentityOptions(), // system-assigned MI + Claims = claimsChallengeJson // when retrying after 401 + } + }); +``` + +## Telemetry + +We rely on server side telemetry for the token revocation features. + +Server dashboards add MI success‑rate with/without cp1. + +## Options as seen in MSAL + +![alt text](capab1.png) + +### reference - [How to use Continuous Access Evaluation enabled APIs in your applications](https://learn.microsoft.com/en-us/entra/identity-platform/app-resilience-continuous-access-evaluation?tabs=dotnet) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs new file mode 100644 index 000000000..14c4a0968 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client; + +namespace Microsoft.Identity.Web +{ + /// + /// **TEST-ONLY.** Allows unit tests to supply a custom . + /// + internal interface IManagedIdentityTestHttpClientFactory + { + IMsalHttpClientFactory Create(); + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt index 5dfd019d9..6770452ef 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ -const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! \ No newline at end of file +const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt index 5dfd019d9..6770452ef 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ -const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! \ No newline at end of file +const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt index 2c5e8d3a5..19d67d4ec 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt @@ -1,2 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.ClientInfoJsonContext \ No newline at end of file +Microsoft.Identity.Web.ClientInfoJsonContext +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt index 2c5e8d3a5..19d67d4ec 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt @@ -1,2 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.ClientInfoJsonContext \ No newline at end of file +Microsoft.Identity.Web.ClientInfoJsonContext +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 2c5e8d3a5..19d67d4ec 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1,2 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.ClientInfoJsonContext \ No newline at end of file +Microsoft.Identity.Web.ClientInfoJsonContext +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 2c5e8d3a5..19d67d4ec 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1,2 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.ClientInfoJsonContext \ No newline at end of file +Microsoft.Identity.Web.ClientInfoJsonContext +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 5dfd019d9..6770452ef 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ -const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! \ No newline at end of file +const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs index e22e7589c..7b07452dc 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Abstractions; @@ -19,6 +21,7 @@ internal partial class TokenAcquisition private readonly ConcurrentDictionary _managedIdentityApplicationsByClientId = new(); private readonly SemaphoreSlim _managedIdSemaphore = new(1, 1); private const string SystemAssignedManagedIdentityKey = "SYSTEM"; + private readonly IManagedIdentityTestHttpClientFactory? _miHttpFactory; /// /// Gets a cached ManagedIdentityApplication object or builds a new one if not found. @@ -62,6 +65,7 @@ internal async Task GetOrBuildManagedIdentityApplic // Build the application application = BuildManagedIdentityApplication( managedIdentityId, + mergedOptions.ClientCapabilities, mergedOptions.ConfidentialClientApplicationOptions.EnablePiiLogging ); @@ -80,17 +84,33 @@ internal async Task GetOrBuildManagedIdentityApplic /// Creates a managed identity client application. /// /// Indicates if system-assigned or user-assigned managed identity is used. + /// Indicates the capabilities of the managed identity application. /// Indicates if logging that may contain personally identifiable information is enabled. /// A managed identity application. - private IManagedIdentityApplication BuildManagedIdentityApplication(ManagedIdentityId managedIdentityId, bool enablePiiLogging) + private IManagedIdentityApplication BuildManagedIdentityApplication( + ManagedIdentityId managedIdentityId, + IEnumerable? capabilities, + bool enablePiiLogging) { - return ManagedIdentityApplicationBuilder + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder .Create(managedIdentityId) .WithLogging( Log, ConvertMicrosoftExtensionsLogLevelToMsal(_logger), - enablePiiLogging: enablePiiLogging) - .Build(); + enablePiiLogging: enablePiiLogging); + + if (capabilities?.Any() == true) + { + miBuilder.WithClientCapabilities(capabilities); + } + + if (_miHttpFactory != null) + { + miBuilder.WithHttpClientFactory(_miHttpFactory.Create()); + } + + return miBuilder.Build(); + } /// diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 9b45a442f..e66a1d296 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -108,6 +108,7 @@ public TokenAcquisition( _credentialsLoader = credentialsLoader; _certificatesObserver = serviceProvider.GetService(); tokenAcquisitionExtensionOptionsMonitor = serviceProvider.GetService>(); + _miHttpFactory = serviceProvider.GetService(); } #if NET6_0_OR_GREATER @@ -499,7 +500,15 @@ public async Task GetAuthenticationResultForAppAsync( mergedOptions, tokenAcquisitionOptions.ManagedIdentity ); - return await managedIdApp.AcquireTokenForManagedIdentity(scope).ExecuteAsync().ConfigureAwait(false); + + var miBuilder = managedIdApp.AcquireTokenForManagedIdentity(scope); + + if (!string.IsNullOrEmpty(tokenAcquisitionOptions.Claims)) + { + miBuilder.WithClaims(tokenAcquisitionOptions.Claims); + } + + return await miBuilder.ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/tests/DevApps/daemon-app/daemon-app-msi/Program.cs b/tests/DevApps/daemon-app/daemon-app-msi/Program.cs new file mode 100644 index 000000000..0733e48c4 --- /dev/null +++ b/tests/DevApps/daemon-app/daemon-app-msi/Program.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using System.Net; +using System.Text.Json; + +// ── 1. bootstrap factory (reads appsettings.json automatically) ───────── +var factory = TokenAcquirerFactory.GetDefaultInstance(); + +// optional console logging +factory.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Warning)); + +// ── 2. register the downstream API using the "AzureKeyVault" section ──── +factory.Services.AddDownstreamApi("AzureKeyVault", + factory.Configuration.GetSection("AzureKeyVault")); +IServiceProvider sp = factory.Build(); +IDownstreamApi api = sp.GetRequiredService(); + +// ── 3. call the vault (app-token path) ────────────────────────────────── +HttpResponseMessage response = await api.CallApiForAppAsync("AzureKeyVault"); + +if (response.StatusCode != HttpStatusCode.OK) +{ + Console.WriteLine($"Vault returned {(int)response.StatusCode} {response.ReasonPhrase}"); + return; +} + +//Get the secret value from the response +using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + +// Check if the "value" property exists and is a string +if (doc.RootElement.TryGetProperty("value", out var valueElement) && valueElement.ValueKind == JsonValueKind.String) +{ + // Retrieve the secret value but do not print it + string secret = valueElement.GetString()!; + + // Optionally, you can check if the secret is not null or empty + if (!string.IsNullOrEmpty(secret)) + { + Console.WriteLine("Secret retrieved successfully (non-null)."); + } + else + { + Console.WriteLine("Secret value was empty."); + } +} diff --git a/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json b/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json new file mode 100644 index 000000000..8b9a0c86a --- /dev/null +++ b/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json @@ -0,0 +1,29 @@ +{ + // authentication settings (apply to the whole app) + "AzureAd": { + // Continuous Access Evaluation capability at app-level + "ClientCapabilities": [ "cp1" ] + }, + + // downstream API settings (per-resource) + "AzureKeyVault": { + "BaseUrl": "https://msidlabs.vault.azure.net/", + "RelativePath": "secrets/msidlab4?api-version=7.4", + "RequestAppToken": true, + "Scopes": [ "https://vault.azure.net/.default" ], + // per request settings + "AcquireTokenOptions": { + "ManagedIdentity": { + // user-assigned MI; omit for system-assigned + "UserAssignedClientId": "4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6" + } + } + }, + + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Information" + } + } +} diff --git a/tests/DevApps/daemon-app/daemon-app-msi/daemon-app-msi.csproj b/tests/DevApps/daemon-app/daemon-app-msi/daemon-app-msi.csproj new file mode 100644 index 000000000..36e7e2b3f --- /dev/null +++ b/tests/DevApps/daemon-app/daemon-app-msi/daemon-app-msi.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + Daemon_app + enable + enable + 13 + false + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/tests/DevApps/daemon-app/daemon-app-msi/readme.md b/tests/DevApps/daemon-app/daemon-app-msi/readme.md new file mode 100644 index 000000000..592c12d45 --- /dev/null +++ b/tests/DevApps/daemon-app/daemon-app-msi/readme.md @@ -0,0 +1,48 @@ +# VM-Hosted Key Vault Secret Retriever + +Tiny console app that runs **inside an Azure VM** configured with a **User-Assigned Managed Identity (UAMI)**. +Its only job is to fetch **Secret** from an Azure Key Vault section in *appsettings.json*. + +--- + +## 1. Prerequisites + +| Requirement | Notes | +|-------------|-------| +| Azure VM | Any OS; the UAMI must be **assigned** to the VM. | +| User-Assigned Managed Identity | Needs ** `Get`** permission on the Key Vault’s **Secrets**. | +| Key Vault | Secret named `secret` (or whatever your *AzureKeyVault* section points to). | +| .NET 8 SDK | Build / run the app locally or on the VM. | +| *appsettings.json* | Contains an `AzureKeyVault` block with `BaseUrl`, `RelativePath`, etc. | + +> 💡 **Least privilege**: grant the UAMI only the `secrets/get` permission. + +--- + +## 2. How the Code Works + +```txt +┌─────────────┐ 1. TokenAcquirerFactory auto-binds +│ appsettings │─── to Azure credentials (UAMI) on the VM. +└─────────────┘ + │ + ▼ +┌────────────────────┐ 2. Register “AzureKeyVault” downstream API +│ DI Service graph │── using the config section. +└────────────────────┘ + │ + ▼ +┌───────────────────────┐ +│ IDownstreamApi.Call │ 3. GET {vault-url}/secrets/secret?api-version=7.4 +└───────────────────────┘ + │ + ▼ +┌───────────────────────┐ +│ HttpResponseMessage │ 4. Parse JSON; extract `value`. +└───────────────────────┘ + │ + ▼ +┌───────────────────────┐ +│ Console logging │ 5. Log *success*. +└───────────────────────┘ +``` diff --git a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpCreator.cs b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpCreator.cs index 19918c8d3..38ee3ebc9 100644 --- a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpCreator.cs +++ b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpCreator.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Globalization; using System.Net; using System.Net.Http; using Microsoft.Identity.Web.Util; @@ -71,5 +73,31 @@ public static MockHttpMessageHandler CreateHandlerToValidatePostData( ResponseMessage = CreateSuccessfulClientCredentialTokenResponseMessage(), }; } + + public static MockHttpMessageHandler CreateMsiTokenHandler( + string accessToken, + string resource = "https://management.azure.com/", + int expiresIn = 3599) + { + string expiresOn = DateTimeOffset.UtcNow + .AddSeconds(expiresIn) + .ToUnixTimeSeconds() + .ToString(CultureInfo.InvariantCulture); + + string json = $@"{{ + ""access_token"": ""{accessToken}"", + ""expires_in"" : ""{expiresIn}"", + ""expires_on"" : ""{expiresOn}"", + ""resource"" : ""{resource}"", + ""token_type"" : ""Bearer"", + ""client_id"" : ""client_id"" + }}"; + + return new MockHttpMessageHandler + { + ExpectedMethod = HttpMethod.Get, + ResponseMessage = CreateSuccessResponseMessage(json) + }; + } } } diff --git a/tests/Microsoft.Identity.Web.Test/FmiTests.cs b/tests/Microsoft.Identity.Web.Test/FmiTests.cs index f40a20ec9..b79083566 100644 --- a/tests/Microsoft.Identity.Web.Test/FmiTests.cs +++ b/tests/Microsoft.Identity.Web.Test/FmiTests.cs @@ -13,6 +13,7 @@ namespace Microsoft.Identity.Web.Test { + [Collection("Run tests - serial")] public class FmiTests { [Fact] diff --git a/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs b/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs new file mode 100644 index 000000000..6ea9ee5b5 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Test.Common.Mocks; +using Microsoft.Identity.Web.TestOnly; +using Xunit; +using Microsoft.Identity.Web.Test; +using NSubstitute; +using System.Collections.Generic; +using System.Net.Http; +using System.Net; +using System.Text; +using System.Security.Claims; +using System.Threading; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.Identity.Web.Tests.Certificateless +{ + [Collection("Run tests - serial")] + public class ManagedIdentityTests + { + private const string Scope = "https://management.azure.com/.default"; + private const string UamiClientId = "04ca4d6a-c720-4ba1-aa06-f6634b73fe7a"; + private const string MockToken = "mocked.access.token"; + private const string CaeClaims = + @"{""access_token"":{""nbf"":{""essential"":true,""value"":""1702682181""}}}"; + + private const string Downstream401Service = "Downstream401"; + private const string FirstToken = "mocked.access.token-1"; + private const string SecondToken = "mocked.access.token-2"; + private const string VaultBaseUrl = "https://my-vault.vault.azure.net/"; + private const string SecretPath = "secrets/mySecret"; + + private sealed record VaultSecret(string Value); + + [Fact] + public async Task ManagedIdentity_ReturnsBearerHeader() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var factory = TokenAcquirerFactory.GetDefaultInstance(); + + var mockHttp = new MockHttpClientFactory(); + + mockHttp.AddMockHandler( + MockHttpCreator.CreateMsiTokenHandler(accessToken: MockToken)); + + // Add the mock handler to the DI container + factory.Services.AddSingleton( + _ => new TestManagedIdentityHttpFactory(mockHttp)); + + IAuthorizationHeaderProvider headerProvider = factory.Build() + .GetRequiredService(); + + // basic mi flow where we get a token + string header = await headerProvider.CreateAuthorizationHeaderForAppAsync( + Scope, + new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + ManagedIdentity = new ManagedIdentityOptions { UserAssignedClientId = UamiClientId }, + } + }); + + Assert.Equal($"Bearer {MockToken}", header); + } + + [Fact] + public async Task ManagedIdentity_WithClaims_HeaderBypassesCache() + { + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + var mockedHttp = new MockHttpClientFactory(); + + // token-1 will be cached, token-2 should be returned when claims force a bypass + mockedHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("token1")); + mockedHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("token2")); + + tokenAcquirerFactory.Services.AddSingleton( + _ => new TestManagedIdentityHttpFactory(mockedHttp)); + + var headerProvider = tokenAcquirerFactory.Build() + .GetRequiredService(); + + // Initial call � no claims, token cached + string header1 = await headerProvider.CreateAuthorizationHeaderForAppAsync( + Scope, + new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + ManagedIdentity = new ManagedIdentityOptions + { + UserAssignedClientId = "UamiClientId2" + } + } + }); + Assert.Equal("Bearer token1", header1); + + // Same UAMI with CAE claims � should bypass cache + string header2 = await headerProvider.CreateAuthorizationHeaderForAppAsync( + Scope, + new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + ManagedIdentity = new ManagedIdentityOptions + { + UserAssignedClientId = "UamiClientId2" + }, + Claims = CaeClaims + } + }); + Assert.Equal("Bearer token2", header2); + } + + [Fact] + public async Task UserAssigned_MI_Caching_and_Claims() + { + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var factory = TokenAcquirerFactory.GetDefaultInstance(); + var mockHttp = new MockHttpClientFactory(); + + // a = token-1 b = cached token (i.e. token-1) + // c = token-2 d = token-3 + mockHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("token-1")); // a + mockHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("token-2")); // c + mockHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("token-3")); // d + + factory.Services.AddSingleton( + _ => new TestManagedIdentityHttpFactory(mockHttp)); + + var provider = factory.Build(); + var tokens = provider.GetRequiredService(); + + // helper + static TokenAcquisitionOptions Uami(string id, string? claims = null) => new() + { + ManagedIdentity = new ManagedIdentityOptions { UserAssignedClientId = id }, + Claims = claims + }; + + // scenario a : first call directed to IdP for uamiA + var r1 = await tokens.GetAuthenticationResultForAppAsync( + Scope, tokenAcquisitionOptions: Uami("uamiA")); + Assert.Equal(TokenSource.IdentityProvider, r1.AuthenticationResultMetadata.TokenSource); + Assert.Equal("token-1", r1.AccessToken); + + // scenario b : same uamiA and no claims gets a cached token + var r2 = await tokens.GetAuthenticationResultForAppAsync( + Scope, tokenAcquisitionOptions: Uami("uamiA")); + Assert.Equal(TokenSource.Cache, r2.AuthenticationResultMetadata.TokenSource); + Assert.Equal("token-1", r2.AccessToken); + + // scenario c : same uamiA + CAE claims gets a token from IdP (bypasses cache) + var r3 = await tokens.GetAuthenticationResultForAppAsync( + Scope, tokenAcquisitionOptions: Uami("uamiA", CaeClaims)); + Assert.Equal(TokenSource.IdentityProvider, r3.AuthenticationResultMetadata.TokenSource); + Assert.Equal("token-2", r3.AccessToken); + + // scenario d : different UAMI (say uamiB) gets a token from IdP + var r4 = await tokens.GetAuthenticationResultForAppAsync( + Scope, tokenAcquisitionOptions: Uami("uamiB")); + Assert.Equal(TokenSource.IdentityProvider, r4.AuthenticationResultMetadata.TokenSource); + Assert.Equal("token-3", r4.AccessToken); + } + + [Fact] + public async Task SystemAssigned_MSI_Forwards_ClientCapabilities_InQuery() + { + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var factory = TokenAcquirerFactory.GetDefaultInstance(); + + // Mock IMDS/MSI: returns a 200 with "access_token" and records the request + var captureHandler = MockHttpCreator.CreateMsiTokenHandler(MockToken); + + var mockHttp = new MockHttpClientFactory(); + mockHttp.AddMockHandler(captureHandler); + factory.Services.AddSingleton( + _ => new TestManagedIdentityHttpFactory(mockHttp)); + + // Enable capabilities cp1,cp2 + factory.Services.Configure(opts => + opts.ClientCapabilities = ["cp1", "cp2"]); + + var tokenAcquirer = factory.Build() + .GetRequiredService(); + + // Act + var result = await tokenAcquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions + { + ManagedIdentity = new ManagedIdentityOptions(), // system-assigned + Claims = CaeClaims + }); + + // Assert + Assert.Equal(MockToken, result.AccessToken); + + // Assert - outbound GET includes xms_cc=cp1%2Ccp2 + // This check can be enabled when MSAL enables cae + //string query = captureHandler.ActualRequestMessage!.RequestUri!.Query; + //Assert.Contains("xms_cc=cp1%2Ccp2", query, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task DownstreamApi_401Claims_TriggersSingleRetry_AndSucceeds() + { + // challenge JSON + string challengeB64 = Base64UrlEncoder.Encode( + Encoding.UTF8.GetBytes(CaeClaims)); + + // authProvider mock + var authProvider = Substitute.For(); + DownstreamApiOptions? capturedOpts = null; + + authProvider.CreateAuthorizationHeaderAsync( + Arg.Any>(), + Arg.Do(o => capturedOpts = o), + Arg.Any(), + Arg.Any()) + .Returns(ci => $"Bearer {FirstToken}", + ci => $"Bearer {SecondToken}"); + + // queue handler: 401 w/ claims - 200 OK + // Id Web will single retry the request on 401 + var queue = new QueueHttpMessageHandler(); + + // 401 response with claims + var r401 = new HttpResponseMessage(HttpStatusCode.Unauthorized); + + // add the claims challenge with error in the header + r401.Headers.WwwAuthenticate.ParseAdd( + $"Bearer realm=\"\", error=\"insufficient_claims\", " + + $"error_description=\"token requires claims\", " + + $"claims=\"{challengeB64}\""); + queue.AddHttpResponseMessage(r401); + + queue.AddHttpResponseMessage(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"value\": \"MockSecretValue\" }", + Encoding.UTF8, "application/json") + }); + + // DI container + var services = new ServiceCollection(); + services.AddHttpClient(Downstream401Service) + .ConfigurePrimaryHttpMessageHandler(() => queue); + services.AddLogging(); + services.AddTokenAcquisition(); + services.AddSingleton(authProvider); + + services.AddDownstreamApi(Downstream401Service, opts => + { + opts.BaseUrl = VaultBaseUrl; + opts.RelativePath = SecretPath; + opts.RequestAppToken = true; + opts.Scopes = [Scope]; + }); + + var sp = services.BuildServiceProvider(); + var api = sp.GetRequiredService(); + + // ACT + VaultSecret? secret = await api.GetForAppAsync(Downstream401Service); + + // ASSERT + Assert.NotNull(secret); + Assert.Equal("MockSecretValue", secret!.Value); // retry succeeded + + await authProvider.Received(2).CreateAuthorizationHeaderAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any()); // called twice + + Assert.Equal(challengeB64, capturedOpts!.AcquireTokenOptions.Claims); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs b/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs new file mode 100644 index 000000000..c6ce6a0f2 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client; + +namespace Microsoft.Identity.Web.Test +{ + internal sealed class TestManagedIdentityHttpFactory : IManagedIdentityTestHttpClientFactory + { + private readonly IMsalHttpClientFactory _msalHttpClientFactory; + public TestManagedIdentityHttpFactory(IMsalHttpClientFactory msalHttpClientFactory) + => _msalHttpClientFactory = msalHttpClientFactory; + public IMsalHttpClientFactory Create() => _msalHttpClientFactory; + } +} diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquirerCollection.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquirerCollection.cs new file mode 100644 index 000000000..3cdc7b182 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquirerCollection.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.Identity.Web.Tests +{ + /// + /// Disables parallel execution for every test-class that + /// is decorated with [Collection("Run tests - serial")]. + /// NOTE: tests that rely on TokenAcquirerFactory / TokenAcquisition (and the + /// IMsalHttpClientFactory mocks they spin-up) share several static / singleton + /// caches. If xUnit runs them in parallel those shared objects collide and the + /// mocks return the wrong handler, producing flaky failures. Putting them all + /// in this “serial” collection forces xUnit to execute them one-by-one. + /// Be cautious: only include tests that really need this, as this will impact + /// the overall test suite running time + /// + [CollectionDefinition("Run tests - serial", DisableParallelization = true)] + public sealed class TokenAcquirerCollection : ICollectionFixture + { + } +} From c3562a388a95b054c3c091dd45dd6711fbfe7594 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson Date: Wed, 4 Jun 2025 14:33:42 -0700 Subject: [PATCH 2/2] pr comments --- .../IManagedIdentityTestHttpClientFactory.cs | 2 +- .../net462/InternalAPI.Unshipped.txt | 4 +-- .../net472/InternalAPI.Unshipped.txt | 4 +-- .../net6.0/InternalAPI.Unshipped.txt | 4 +-- .../net7.0/InternalAPI.Unshipped.txt | 4 +-- .../net8.0/InternalAPI.Unshipped.txt | 4 +-- .../net9.0/InternalAPI.Unshipped.txt | 4 +-- .../netstandard2.0/InternalAPI.Unshipped.txt | 4 +-- .../TokenAcquisition.ManagedIdentity.cs | 1 + .../TokenAcquisition.cs | 1 + .../daemon-app/daemon-app-msi/readme.md | 32 ++++--------------- .../TestManagedIdentityHttpFactory.cs | 1 + 12 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs index 14c4a0968..33b6f4f04 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IManagedIdentityTestHttpClientFactory.cs @@ -3,7 +3,7 @@ using Microsoft.Identity.Client; -namespace Microsoft.Identity.Web +namespace Microsoft.Identity.Web.TestOnly { /// /// **TEST-ONLY.** Allows unit tests to supply a custom . diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt index 6770452ef..d83645276 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1,3 +1,3 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt index 6770452ef..d83645276 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1,3 +1,3 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt index 19d67d4ec..25f816d66 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/InternalAPI.Unshipped.txt @@ -1,4 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! Microsoft.Identity.Web.ClientInfoJsonContext -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt index 19d67d4ec..25f816d66 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/InternalAPI.Unshipped.txt @@ -1,4 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! Microsoft.Identity.Web.ClientInfoJsonContext -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 19d67d4ec..25f816d66 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1,4 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! Microsoft.Identity.Web.ClientInfoJsonContext -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 19d67d4ec..25f816d66 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1,4 +1,4 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! Microsoft.Identity.Web.ClientInfoJsonContext -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 6770452ef..d83645276 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1,3 +1,3 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory -Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory +Microsoft.Identity.Web.TestOnly.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs index 7b07452dc..02192cf8d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.ManagedIdentity.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Web.TestOnly; using Microsoft.IdentityModel.Tokens; namespace Microsoft.Identity.Web diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index e66a1d296..dfe17bcc5 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -21,6 +21,7 @@ using Microsoft.Identity.Client.Advanced; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Web.Experimental; +using Microsoft.Identity.Web.TestOnly; using Microsoft.Identity.Web.TokenCacheProviders; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Microsoft.IdentityModel.JsonWebTokens; diff --git a/tests/DevApps/daemon-app/daemon-app-msi/readme.md b/tests/DevApps/daemon-app/daemon-app-msi/readme.md index 592c12d45..ffeb4469b 100644 --- a/tests/DevApps/daemon-app/daemon-app-msi/readme.md +++ b/tests/DevApps/daemon-app/daemon-app-msi/readme.md @@ -1,7 +1,7 @@ # VM-Hosted Key Vault Secret Retriever Tiny console app that runs **inside an Azure VM** configured with a **User-Assigned Managed Identity (UAMI)**. -Its only job is to fetch **Secret** from an Azure Key Vault section in *appsettings.json*. +Its only job is to fetch **Secret** from an Azure Key Vault section specified in the *appsettings.json*. --- @@ -21,28 +21,10 @@ Its only job is to fetch **Secret** from an Azure Key Vault section in *appsetti ## 2. How the Code Works -```txt -┌─────────────┐ 1. TokenAcquirerFactory auto-binds -│ appsettings │─── to Azure credentials (UAMI) on the VM. -└─────────────┘ - │ - ▼ -┌────────────────────┐ 2. Register “AzureKeyVault” downstream API -│ DI Service graph │── using the config section. -└────────────────────┘ - │ - ▼ -┌───────────────────────┐ -│ IDownstreamApi.Call │ 3. GET {vault-url}/secrets/secret?api-version=7.4 -└───────────────────────┘ - │ - ▼ -┌───────────────────────┐ -│ HttpResponseMessage │ 4. Parse JSON; extract `value`. -└───────────────────────┘ - │ - ▼ -┌───────────────────────┐ -│ Console logging │ 5. Log *success*. -└───────────────────────┘ +```mermaid +flowchart TD + A["1️⃣ **appsettings.json**
(TokenAcquirerFactory auto-binds UAMI creds)"] --> B["2️⃣ **DI Container**
(register “AzureKeyVault” downstream API)"] + B --> C["3️⃣ **IDownstreamApi.Call**
GET {vault-url}/secrets/secret?api-version=7.4"] + C --> D["4️⃣ **HttpResponseMessage**
Parse JSON → extract `value`"] + D --> E["5️⃣ **Console logging**
Log *success*"] ``` diff --git a/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs b/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs index c6ce6a0f2..ddad6546c 100644 --- a/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs +++ b/tests/Microsoft.Identity.Web.Test/TestManagedIdentityHttpFactory.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.Identity.Client; +using Microsoft.Identity.Web.TestOnly; namespace Microsoft.Identity.Web.Test {