From 2f87a894f7f581facf3ccdae8b2b51082b49608a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:22:17 -0800 Subject: [PATCH 01/12] Update agent identity coordinates in E2E tests after deauth (#3640) * Initial plan * Update agent identity coordinates for E2E tests after deauth * Update agent identity coordinates in Sidecar.Tests * Removing the the need for scopes/roles in SideCar E2E tests Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../AgentApplications/AgentUserIdentityTestscs.cs | 8 ++++---- .../AgentApplications/AutonomousAgentTests.cs | 4 ++-- tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs | 4 ++-- tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs | 5 +++-- .../Sidecar.Tests/SidecarEndpointsE2ETests.cs | 10 +++++----- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs b/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs index 0c6c7f705..dad419cc2 100644 --- a/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs +++ b/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs @@ -20,9 +20,9 @@ public class AgentUserIdentityTests { string instance = "https://login.microsoftonline.com/"; string tenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - string agentApplication = "c4b2d4d9-9257-4c1a-a5c0-0a4907c83411"; // Replace with the actual agent application client ID - string agentIdentity = "44250d7d-2362-4fba-9ba0-49c19ae270e0"; // Replace with the actual agent identity - string userUpn = "aui3@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. + string agentApplication = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Replace with the actual agent application client ID + string agentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity + string userUpn = "agentuser1@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. [Fact] public async Task AgentUserIdentityGetsTokenForGraphAsync() @@ -254,7 +254,7 @@ public async Task AgentUserIdentityGetsTokenForGraphWithCacheAsync() [Fact] public async Task AgentUserIdentityGetsTokenForGraphByUserIdAsync() { - string userOid = "04ea4dcd-f314-476f-be31-a13707cdd11e"; // Replace with the actual user OID. + string userOid = "03d648e4-2e01-4dfb-b21d-81eb678fbcf4"; // Replace with the actual user OID. IServiceCollection services = new ServiceCollection(); diff --git a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs index 763f627d5..badfd44c6 100644 --- a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs +++ b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs @@ -24,7 +24,7 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() configuration["AzureAd:Instance"] = "https://login.microsoftonline.com/"; configuration["AzureAd:TenantId"] = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; - configuration["AzureAd:ClientId"] = "d15884b6-a447-4dd5-a5a5-a668c49f6300"; // Agent application. + configuration["AzureAd:ClientId"] = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Agent application. configuration["AzureAd:ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName"; configuration["AzureAd:ClientCredentials:0:CertificateStorePath"] = "LocalMachine/My"; configuration["AzureAd:ClientCredentials:0:CertificateDistinguishedName"] = "CN=LabAuth.MSIDLab.com"; @@ -39,7 +39,7 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() services.AddMicrosoftGraph(); // If you want to call Microsoft Graph var serviceProvider = services.BuildServiceProvider(); - string agentIdentity = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; // Replace with the actual agent identity + string agentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity //// Get an authorization header and handle the call to the downstream API yoursel IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetService()!; diff --git a/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs b/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs index 4bd10bdb6..fc8a86de6 100644 --- a/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs +++ b/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs @@ -18,8 +18,8 @@ public async Task GetFicTokensTestsAsync() { string instance = "https://login.microsoftonline.com/"; string tenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - string agentApplication = "d15884b6-a447-4dd5-a5a5-a668c49f6300"; // Replace with the actual agent application client ID - string agentIdentity = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; // Replace with the actual agent identity + string agentApplication = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Replace with the actual agent application client ID + string agentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity IServiceCollection services = new ServiceCollection(); diff --git a/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs b/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs index dcf5632ce..32d3c0898 100644 --- a/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs +++ b/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs @@ -25,8 +25,9 @@ internal SidecarApiFactory(Action? configureOptions) { { "AzureAd:Instance", "https://login.microsoftonline.com/" }, { "AzureAd:TenantId", "31a58c3b-ae9c-4448-9e8f-e9e143e800df" }, - { "AzureAd:ClientId", "d15884b6-a447-4dd5-a5a5-a668c49f6300" }, - { "AzureAd:Audience", "d15884b6-a447-4dd5-a5a5-a668c49f6300" }, + { "AzureAd:ClientId", "d05619c9-dbf2-4e60-95fd-cc75dd0db451" }, + { "AzureAd:Audience", "d05619c9-dbf2-4e60-95fd-cc75dd0db451" }, + { "AzureAd:AllowWebApiToBeAuthorizedByACL", "true" }, { "AzureAd:ClientCredentials:0:SourceType", "StoreWithDistinguishedName" }, { "AzureAd:ClientCredentials:0:CertificateStorePath", "LocalMachine/My" }, { "AzureAd:ClientCredentials:0:CertificateDistinguishedName", "CN=LabAuth.MSIDLab.com" }, // Replace with the subject name of your certificate diff --git a/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs b/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs index a89b5b1e9..180dce5ab 100644 --- a/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs +++ b/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs @@ -23,10 +23,10 @@ public class SidecarEndpointsE2ETests : IClassFixture public SidecarEndpointsE2ETests(SidecarApiFactory factory) => _factory = factory; const string TenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - const string AgentApplication = "d15884b6-a447-4dd5-a5a5-a668c49f6300"; // Replace with the actual agent application client ID - const string AgentIdentity = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; // Replace with the actual agent identity - const string UserUpn = "aui1@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. - string UserOid = "51c1aa1c-f6d0-4a92-936c-cadb27b717f2"; // Replace with the actual user OID. + const string AgentApplication = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Replace with the actual agent application client ID + const string AgentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity + const string UserUpn = "agentuser1@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. + string UserOid = "03d648e4-2e01-4dfb-b21d-81eb678fbcf4"; // Replace with the actual user OID. [Fact] public async Task Validate_WhenBadTokenAsync() @@ -187,7 +187,7 @@ private static async Task GetAuthorizationHeaderToCallTheSideCarAsync() IServiceProvider serviceProvider = services.BuildServiceProvider(); IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); - string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("api://d15884b6-a447-4dd5-a5a5-a668c49f6300/.default", + string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("api://d05619c9-dbf2-4e60-95fd-cc75dd0db451/.default", new AuthorizationHeaderProviderOptions() { AcquireTokenOptions = new AcquireTokenOptions() From 68c9fb9319902a06b6d015e2304be0d0c6d5448d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:54:10 -0800 Subject: [PATCH 02/12] Bump express in /tests/DevApps/SidecarAdapter/typescript (#3636) Bumps [express](https://github.com/expressjs/express) from 5.1.0 to 5.2.0. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/v5.1.0...v5.2.0) --- updated-dependencies: - dependency-name: express dependency-version: 5.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur --- .../typescript/package-lock.json | 107 +++++++++--------- .../SidecarAdapter/typescript/package.json | 2 +- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/tests/DevApps/SidecarAdapter/typescript/package-lock.json b/tests/DevApps/SidecarAdapter/typescript/package-lock.json index 9dda437e3..88c1488ab 100644 --- a/tests/DevApps/SidecarAdapter/typescript/package-lock.json +++ b/tests/DevApps/SidecarAdapter/typescript/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "express": "^5.1.0" + "express": "^5.2.0" }, "devDependencies": { "@azure/msal-node": "^3.8.0", @@ -1236,6 +1236,7 @@ "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", @@ -1582,6 +1583,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1657,23 +1659,27 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2135,6 +2141,7 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2314,18 +2321,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz", + "integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -2669,40 +2677,39 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -2860,6 +2867,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3492,36 +3500,20 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3959,6 +3951,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4037,6 +4030,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -4084,6 +4078,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4167,6 +4162,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4283,6 +4279,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/tests/DevApps/SidecarAdapter/typescript/package.json b/tests/DevApps/SidecarAdapter/typescript/package.json index b8ddde346..f7c8f7632 100644 --- a/tests/DevApps/SidecarAdapter/typescript/package.json +++ b/tests/DevApps/SidecarAdapter/typescript/package.json @@ -26,6 +26,6 @@ "vitest": "^3.2.4" }, "dependencies": { - "express": "^5.1.0" + "express": "^5.2.0" } } From b496a268f2cfa46f08f497820c0aa6536f4b995b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:41:53 -0800 Subject: [PATCH 03/12] Fix tenant not propagated in credential FIC acquisition (#3633) * Initial plan * Fix tenant propagation in OidcIdpSignedAssertionProvider credential FIC acquisition Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Address code review feedback: improve URI handling and validate token endpoint pattern Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Add unit tests for ExtractTenantFromTokenEndpointIfSameInstance method and make it internal Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Fix warnings in owin test apps * Add E2E test for tenant override with common/organizations authority Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Simplify the Autonomous agent tests * Update tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs * Inproving the autonomous test with tenant override --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur --- .../OidcIdpSignedAssertionProvider.cs | 65 +++++++++- .../net10.0/InternalAPI.Unshipped.txt | 1 + .../net462/InternalAPI.Unshipped.txt | 1 + .../net472/InternalAPI.Unshipped.txt | 1 + .../net8.0/InternalAPI.Unshipped.txt | 1 + .../net9.0/InternalAPI.Unshipped.txt | 1 + .../netstandard2.0/InternalAPI.Unshipped.txt | 1 + .../DevApps/aspnet-mvc/OwinWebApi/Web.config | 16 ++- .../DevApps/aspnet-mvc/OwinWebApp/Web.config | 16 ++- .../AgentApplications/AutonomousAgentTests.cs | 25 ++-- .../OidcIdpSignedAssertionProviderTests.cs | 116 ++++++++++++++++++ 11 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs diff --git a/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs index 56286193b..ba37998dc 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs +++ b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs @@ -55,9 +55,17 @@ protected override async Task GetClientAssertionAsync(Assertion if (assertionRequestOptions != null && !string.IsNullOrEmpty(assertionRequestOptions.ClientAssertionFmiPath)) { + // Extract tenant from TokenEndpoint if available and if it's from the same cloud instance. + // This enables tenant override propagation while preserving cross-cloud scenarios. + // TokenEndpoint format: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token + string? tenant = ExtractTenantFromTokenEndpointIfSameInstance( + assertionRequestOptions.TokenEndpoint, + _options.Instance); + acquireTokenOptions = new AcquireTokenOptions() { - FmiPath = assertionRequestOptions.ClientAssertionFmiPath + FmiPath = assertionRequestOptions.ClientAssertionFmiPath, + Tenant = tenant }; } @@ -83,5 +91,60 @@ protected override async Task GetClientAssertionAsync(Assertion } return clientAssertion; } + + /// + /// Extracts the tenant from a token endpoint URL if the endpoint is from the same cloud instance. + /// This enables tenant override propagation while preserving cross-cloud scenarios. + /// + /// Token endpoint URL in the format https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token + /// The configured instance URL (e.g., https://login.microsoftonline.com/) + /// The tenant ID if the endpoint is from the same instance, otherwise null. + internal static string? ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) + { + if (string.IsNullOrEmpty(tokenEndpoint) || string.IsNullOrEmpty(configuredInstance)) + { + return null; + } + + try + { + var endpointUri = new Uri(tokenEndpoint!); + + // Safely construct instance URI by trimming trailing slash + var normalizedInstance = configuredInstance!.TrimEnd('/'); + var instanceUri = new Uri(normalizedInstance); + + // Only extract tenant if the host matches (same cloud instance) + if (!string.Equals(endpointUri.Host, instanceUri.Host, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // TokenEndpoint format: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token + // Validate the path follows the expected pattern before extracting tenant. + var pathSegments = endpointUri.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + // Expected pattern: [tenantId, oauth2, v2.0, token] or similar + // We need at least the tenant segment and some oauth2 path segments + if (pathSegments.Length >= 2) + { + // Verify this looks like a token endpoint (contains "oauth2" somewhere after tenant) + for (int i = 1; i < pathSegments.Length; i++) + { + if (string.Equals(pathSegments[i], "oauth2", StringComparison.OrdinalIgnoreCase)) + { + // Found oauth2 segment, the first segment is likely the tenant + return pathSegments[0]; + } + } + } + } + catch (UriFormatException) + { + // Invalid URI, return null + } + + return null; + } } } diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Unshipped.txt index 7dc5c5811..4e52973d2 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt index 7dc5c5811..4e52973d2 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt index 7dc5c5811..4e52973d2 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 7dc5c5811..4e52973d2 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 7dc5c5811..4e52973d2 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 7dc5c5811..4e52973d2 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config index 0715cdcd8..e7210d1c3 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config @@ -20,6 +20,10 @@ + + + + @@ -78,7 +82,7 @@ - + @@ -94,7 +98,7 @@ - + @@ -102,19 +106,19 @@ - + - + - + - + diff --git a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config index 2904757e1..35391c616 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config @@ -21,6 +21,10 @@ + + + + @@ -79,7 +83,7 @@ - + @@ -95,7 +99,7 @@ - + @@ -103,19 +107,19 @@ - + - + - + - + diff --git a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs index badfd44c6..9d4bf93e3 100644 --- a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs +++ b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs @@ -8,7 +8,6 @@ using Microsoft.Graph; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Web; -using Microsoft.Identity.Web.TokenCacheProviders.Distributed; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Microsoft.IdentityModel.Tokens; @@ -16,14 +15,17 @@ namespace AgentApplicationsTests { public class AutonomousAgentTests { - [Fact] - public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() + const string overriddenTenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; + [Theory] + [InlineData("organizations")] + [InlineData("31a58c3b-ae9c-4448-9e8f-e9e143e800df")] + public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync(string configuredTenantId) { IServiceCollection services = new ServiceCollection(); IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); configuration["AzureAd:Instance"] = "https://login.microsoftonline.com/"; - configuration["AzureAd:TenantId"] = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; + configuration["AzureAd:TenantId"] = configuredTenantId; // Set to the GUID or organizations configuration["AzureAd:ClientId"] = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Agent application. configuration["AzureAd:ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName"; configuration["AzureAd:ClientCredentials:0:CertificateStorePath"] = "LocalMachine/My"; @@ -44,6 +46,10 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() //// Get an authorization header and handle the call to the downstream API yoursel IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetService()!; AuthorizationHeaderProviderOptions options = new AuthorizationHeaderProviderOptions().WithAgentIdentity(agentIdentity); + if (configuredTenantId == "organizations") + { + options.AcquireTokenOptions.Tenant = overriddenTenantId; + } //// Request user tokens in autonomous agents. string authorizationHeaderWithAppToken = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default", options); @@ -56,7 +62,7 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() // Verify the token does not represent an agent user identity using the extension method Assert.False(claimsIdentity.IsAgentUserIdentity()); - + // Verify we can retrieve the parent agent blueprint if present string? parentBlueprint = claimsIdentity.GetParentAgentBlueprint(); string agentApplication = configuration["AzureAd:ClientId"]!; @@ -65,10 +71,11 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() //// If you want to call Microsoft Graph, just inject and use the Microsoft Graph SDK with the agent identity. GraphServiceClient graphServiceClient = serviceProvider.GetRequiredService(); var apps = await graphServiceClient.Applications.GetAsync(r => r.Options.WithAuthenticationOptions(options => - { - options.WithAgentIdentity(agentIdentity); - options.RequestAppToken = true; - })); + { + options.WithAgentIdentity(agentIdentity); + options.RequestAppToken = true; + options.AcquireTokenOptions.Tenant = configuredTenantId == "organizations" ? overriddenTenantId : null; + })); Assert.NotNull(apps); //// If you want to call downstream APIs letting IdWeb handle authentication. diff --git a/tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs new file mode 100644 index 000000000..3f708e4ec --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web.OidcFic; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class OidcIdpSignedAssertionProviderTests + { + [Theory] + [InlineData("https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.com/", "my-tenant-id")] + [InlineData("https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/v2.0/token", "https://login.microsoftonline.com", "contoso.onmicrosoft.com")] + [InlineData("https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc/oauth2/v2.0/token", "https://login.microsoftonline.com/", "12345678-1234-1234-1234-123456789abc")] + public void ExtractTenantFromTokenEndpointIfSameInstance_SameInstance_ReturnsTenant( + string tokenEndpoint, + string configuredInstance, + string expectedTenant) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Equal(expectedTenant, result); + } + + [Theory] + [InlineData("https://login.microsoftonline.us/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.com/", null)] + [InlineData("https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.us/", null)] + [InlineData("https://login.chinacloudapi.cn/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.com/", null)] + public void ExtractTenantFromTokenEndpointIfSameInstance_DifferentInstance_ReturnsNull( + string tokenEndpoint, + string configuredInstance, + string? expectedResult) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(null, "https://login.microsoftonline.com/")] + [InlineData("", "https://login.microsoftonline.com/")] + [InlineData("https://login.microsoftonline.com/tenant/oauth2/v2.0/token", null)] + [InlineData("https://login.microsoftonline.com/tenant/oauth2/v2.0/token", "")] + [InlineData(null, null)] + [InlineData("", "")] + public void ExtractTenantFromTokenEndpointIfSameInstance_NullOrEmptyInputs_ReturnsNull( + string? tokenEndpoint, + string? configuredInstance) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("not-a-valid-uri", "https://login.microsoftonline.com/")] + [InlineData("https://login.microsoftonline.com/tenant/oauth2/v2.0/token", "not-a-valid-uri")] + public void ExtractTenantFromTokenEndpointIfSameInstance_InvalidUri_ReturnsNull( + string tokenEndpoint, + string configuredInstance) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("https://login.microsoftonline.com/oauth2/v2.0/token", "https://login.microsoftonline.com/")] + [InlineData("https://login.microsoftonline.com/", "https://login.microsoftonline.com/")] + public void ExtractTenantFromTokenEndpointIfSameInstance_NoTenantInPath_ReturnsNull( + string tokenEndpoint, + string configuredInstance) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ExtractTenantFromTokenEndpointIfSameInstance_ValidatesOAuth2Pattern() + { + // Arrange + // This endpoint has a tenant but no oauth2 segment - should return null + var tokenEndpoint = "https://login.microsoftonline.com/my-tenant/some-other-path"; + var configuredInstance = "https://login.microsoftonline.com/"; + + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + } +} From 88ec4ff66816467ba16813105fcbe92039f15fda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:32:56 +0000 Subject: [PATCH 04/12] Bump jws from 3.2.2 to 3.2.3 in /tests/DevApps/SidecarAdapter/typescript (#3641) Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3. - [Release notes](https://github.com/brianloveswords/node-jws/releases) - [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md) - [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3) --- updated-dependencies: - dependency-name: jws dependency-version: 3.2.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/DevApps/SidecarAdapter/typescript/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/DevApps/SidecarAdapter/typescript/package-lock.json b/tests/DevApps/SidecarAdapter/typescript/package-lock.json index 88c1488ab..4c858062a 100644 --- a/tests/DevApps/SidecarAdapter/typescript/package-lock.json +++ b/tests/DevApps/SidecarAdapter/typescript/package-lock.json @@ -2949,13 +2949,13 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, From 55b01e3573587c2ce3a05fe02b6ef1731c7c6b87 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 08:48:09 -0800 Subject: [PATCH 05/12] Add meaningful error message when identity configuration is missing (#3637) * Add meaningful error message when identity configuration is missing (fixes #2921) Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> * Remove non-existent aka.ms link from error message Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> * Re-add aka.ms link to error message Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: neha-bhargava <61847233+neha-bhargava@users.noreply.github.com> --- .../IDWebErrorMessage.cs | 3 ++ .../net10.0/InternalAPI.Unshipped.txt | 1 + .../net462/InternalAPI.Unshipped.txt | 1 + .../net472/InternalAPI.Unshipped.txt | 1 + .../net8.0/InternalAPI.Unshipped.txt | 1 + .../net9.0/InternalAPI.Unshipped.txt | 1 + .../netstandard2.0/InternalAPI.Unshipped.txt | 1 + .../TokenAcquisition.cs | 13 ++++++- .../MergedOptionsTests.cs | 21 ++++++++++ .../TokenAcquisitionTests.cs | 38 +++++++++++++++++++ 10 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs index d01e300ae..b78817e50 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs @@ -66,6 +66,9 @@ internal static class IDWebErrorMessage "StoreLocation must be one of 'CurrentUser', 'LocalMachine'. " + "StoreName must be empty or one of '{0}'. "; + // Configuration Validation IDW10700+ = "IDW10700+:" + public const string MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. "; + // Obsolete messages IDW10800 = "IDW10800:" public const string AadIssuerValidatorGetIssuerValidatorIsObsolete = "IDW10800: Use MicrosoftIdentityIssuerValidatorFactory.GetAadIssuerValidator. See https://aka.ms/ms-id-web/1.2.0. "; public const string InitializeAsyncIsObsolete = "IDW10801: Use Initialize instead. See https://aka.ms/ms-id-web/1.9.0. "; diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt index ef0c8077f..f8fb163a7 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt @@ -9,3 +9,4 @@ static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft. static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! 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 fc4ec4b18..92dfc22fd 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -7,3 +7,4 @@ static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft. static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! 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 fc4ec4b18..92dfc22fd 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -7,3 +7,4 @@ static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft. static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! 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 fc4ec4b18..92dfc22fd 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 @@ -7,3 +7,4 @@ static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft. static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! 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 fc4ec4b18..92dfc22fd 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 @@ -7,3 +7,4 @@ static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft. static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! 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 fc4ec4b18..92dfc22fd 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 @@ -7,3 +7,4 @@ static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft. static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 9d9fe438f..122c81bf0 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -594,7 +594,8 @@ public async Task GetAuthenticationResultForAppAsync( } // Apply tenant override only for AAD authorities and only if non-empty - if (!mergedOptions.Instance.Contains(Constants.CiamAuthoritySuffix + if (!string.IsNullOrEmpty(mergedOptions.Instance) && + !mergedOptions.Instance.Contains(Constants.CiamAuthoritySuffix #if NET6_0_OR_GREATER , StringComparison.OrdinalIgnoreCase #endif @@ -938,6 +939,16 @@ private async Task BuildConfidentialClientApplic { mergedOptions.PrepareAuthorityInstanceForMsal(); + // Validate that we have enough configuration to build an authority + // When PreserveAuthority is true, we use Authority directly, so PreparedInstance is not required + // When IsB2C is true, we still need PreparedInstance + if (!mergedOptions.PreserveAuthority && + string.IsNullOrEmpty(mergedOptions.PreparedInstance) && + string.IsNullOrEmpty(mergedOptions.Authority)) + { + throw new ArgumentException(IDWebErrorMessage.MissingIdentityConfiguration); + } + try { ConfidentialClientApplicationBuilder builder = ConfidentialClientApplicationBuilder diff --git a/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs b/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs index d207c8138..732908df0 100644 --- a/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs @@ -292,5 +292,26 @@ public void PrepareAuthorityInstanceForMsal_DoesNotParseAuthority_WhenInstanceAn Assert.Equal("organizations", options.TenantId); // TenantId remains unchanged Assert.Equal("https://login.microsoftonline.us/", options.PreparedInstance); // PreparedInstance based on original Instance } + + [Fact] + public void PrepareAuthorityInstanceForMsal_LeavesNullPreparedInstance_WhenNoConfigurationProvided() + { + // This test verifies the scenario from issue #2921 where a misconfigured key + // (e.g., "ManagedIdentity " with trailing space instead of "ManagedIdentity") + // results in null Instance and null Authority, which should leave PreparedInstance as null + + // Arrange + var options = new MergedOptions(); + // Simulating the scenario where configuration keys have typos and don't bind correctly + + // Act + options.PrepareAuthorityInstanceForMsal(); + + // Assert + Assert.Null(options.Instance); + Assert.Null(options.TenantId); + Assert.Null(options.Authority); + Assert.Null(options.PreparedInstance); + } } } diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs index bc8ef2fd7..c9a515a8e 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs @@ -150,5 +150,43 @@ private TokenAcquirerFactory InitTokenAcquirerFactory() return tokenAcquirerFactory; } + + /// + /// Tests that when identity configuration is missing (simulating a misconfigured key like "ManagedIdentity " with trailing space), + /// a meaningful ArgumentException is thrown instead of a NullReferenceException. + /// This addresses issue #2921. + /// + [Fact] + public async Task GetAuthenticationResultForAppAsync_ThrowsMeaningfulError_WhenConfigurationIsMissing() + { + // Arrange - Create a factory with missing identity configuration + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.Configure(options => + { + // Intentionally NOT setting Instance, TenantId, or Authority + // This simulates the scenario where configuration keys have typos + // (e.g., "ManagedIdentity " instead of "ManagedIdentity") + options.ClientId = "test-client-id"; + options.ClientCredentials = [new CredentialDescription() + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "someSecret" + }]; + }); + + tokenAcquirerFactory.Services.AddSingleton(); + + IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + // Act & Assert - Should throw ArgumentException with meaningful message + var exception = await Assert.ThrowsAsync(async () => + await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync( + "https://graph.microsoft.com/.default", + new AuthorizationHeaderProviderOptions())); + + Assert.StartsWith(IDWebErrorMessage.MissingIdentityConfiguration, exception.Message, System.StringComparison.Ordinal); + } } } From 648a5059f2670c50e941d54989500e0d8899d94e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:47:23 -0800 Subject: [PATCH 06/12] Update E2E agent identity configuration to new tenant (#3646) * Initial plan * Update agent identity configuration to new tenant and credentials Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Fix AgentIdentity to use correct value (ab18ca07-d139-4840-8b3b-4be9610c6ed5) Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Fix User Object ID to complete GUID (a02b9a5b-ea57-40c9-bf00-8aa631b549ad) Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur --- .../AgentApplications/AgentUserIdentityTestscs.cs | 10 +++++----- .../AgentApplications/AutonomousAgentTests.cs | 8 ++++---- .../AgentApplications/GetFicAsyncTests.cs | 6 +++--- tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs | 6 +++--- .../Sidecar.Tests/SidecarEndpointsE2ETests.cs | 14 +++++++------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs b/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs index dad419cc2..f1fed66d1 100644 --- a/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs +++ b/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs @@ -19,10 +19,10 @@ namespace AgentApplicationsTests public class AgentUserIdentityTests { string instance = "https://login.microsoftonline.com/"; - string tenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - string agentApplication = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Replace with the actual agent application client ID - string agentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity - string userUpn = "agentuser1@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. + string tenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; // Replace with your tenant ID + string agentApplication = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Replace with the actual agent application client ID + string agentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity + string userUpn = "agentuser1@id4slab1.onmicrosoft.com"; // Replace with the actual user upn. [Fact] public async Task AgentUserIdentityGetsTokenForGraphAsync() @@ -254,7 +254,7 @@ public async Task AgentUserIdentityGetsTokenForGraphWithCacheAsync() [Fact] public async Task AgentUserIdentityGetsTokenForGraphByUserIdAsync() { - string userOid = "03d648e4-2e01-4dfb-b21d-81eb678fbcf4"; // Replace with the actual user OID. + string userOid = "a02b9a5b-ea57-40c9-bf00-8aa631b549ad"; // Replace with the actual user OID. IServiceCollection services = new ServiceCollection(); diff --git a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs index 9d4bf93e3..1211239e2 100644 --- a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs +++ b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs @@ -15,10 +15,10 @@ namespace AgentApplicationsTests { public class AutonomousAgentTests { - const string overriddenTenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; + const string overriddenTenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; [Theory] [InlineData("organizations")] - [InlineData("31a58c3b-ae9c-4448-9e8f-e9e143e800df")] + [InlineData("10c419d4-4a50-45b2-aa4e-919fb84df24f")] public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync(string configuredTenantId) { IServiceCollection services = new ServiceCollection(); @@ -26,7 +26,7 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync(st configuration["AzureAd:Instance"] = "https://login.microsoftonline.com/"; configuration["AzureAd:TenantId"] = configuredTenantId; // Set to the GUID or organizations - configuration["AzureAd:ClientId"] = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Agent application. + configuration["AzureAd:ClientId"] = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Agent application. configuration["AzureAd:ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName"; configuration["AzureAd:ClientCredentials:0:CertificateStorePath"] = "LocalMachine/My"; configuration["AzureAd:ClientCredentials:0:CertificateDistinguishedName"] = "CN=LabAuth.MSIDLab.com"; @@ -41,7 +41,7 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync(st services.AddMicrosoftGraph(); // If you want to call Microsoft Graph var serviceProvider = services.BuildServiceProvider(); - string agentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity + string agentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity //// Get an authorization header and handle the call to the downstream API yoursel IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetService()!; diff --git a/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs b/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs index fc8a86de6..1de0e3d34 100644 --- a/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs +++ b/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs @@ -17,9 +17,9 @@ public class GetFicAsyncTests public async Task GetFicTokensTestsAsync() { string instance = "https://login.microsoftonline.com/"; - string tenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - string agentApplication = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Replace with the actual agent application client ID - string agentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity + string tenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; // Replace with your tenant ID + string agentApplication = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Replace with the actual agent application client ID + string agentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity IServiceCollection services = new ServiceCollection(); diff --git a/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs b/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs index 32d3c0898..cda4ec45f 100644 --- a/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs +++ b/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs @@ -24,9 +24,9 @@ internal SidecarApiFactory(Action? configureOptions) builder.AddInMemoryCollection(new Dictionary { { "AzureAd:Instance", "https://login.microsoftonline.com/" }, - { "AzureAd:TenantId", "31a58c3b-ae9c-4448-9e8f-e9e143e800df" }, - { "AzureAd:ClientId", "d05619c9-dbf2-4e60-95fd-cc75dd0db451" }, - { "AzureAd:Audience", "d05619c9-dbf2-4e60-95fd-cc75dd0db451" }, + { "AzureAd:TenantId", "10c419d4-4a50-45b2-aa4e-919fb84df24f" }, + { "AzureAd:ClientId", "aab5089d-e764-47e3-9f28-cc11c2513821" }, + { "AzureAd:Audience", "aab5089d-e764-47e3-9f28-cc11c2513821" }, { "AzureAd:AllowWebApiToBeAuthorizedByACL", "true" }, { "AzureAd:ClientCredentials:0:SourceType", "StoreWithDistinguishedName" }, { "AzureAd:ClientCredentials:0:CertificateStorePath", "LocalMachine/My" }, diff --git a/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs b/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs index 180dce5ab..b9d99ed8d 100644 --- a/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs +++ b/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs @@ -22,11 +22,11 @@ public class SidecarEndpointsE2ETests : IClassFixture public SidecarEndpointsE2ETests(SidecarApiFactory factory) => _factory = factory; - const string TenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - const string AgentApplication = "d05619c9-dbf2-4e60-95fd-cc75dd0db451"; // Replace with the actual agent application client ID - const string AgentIdentity = "edbfbbe7-d240-40dd-aee2-435201dbaa9c"; // Replace with the actual agent identity - const string UserUpn = "agentuser1@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. - string UserOid = "03d648e4-2e01-4dfb-b21d-81eb678fbcf4"; // Replace with the actual user OID. + const string TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; // Replace with your tenant ID + const string AgentApplication = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Replace with the actual agent application client ID + const string AgentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity + const string UserUpn = "agentuser1@id4slab1.onmicrosoft.com"; // Replace with the actual user upn. + string UserOid = "a02b9a5b-ea57-40c9-bf00-8aa631b549ad"; // Replace with the actual user OID. [Fact] public async Task Validate_WhenBadTokenAsync() @@ -175,7 +175,7 @@ private static async Task GetAuthorizationHeaderToCallTheSideCarAsync() IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); services.AddSingleton(configuration); configuration["Instance"] = "https://login.microsoftonline.com/"; - configuration["TenantId"] = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; + configuration["TenantId"] = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; configuration["ClientId"] = "5cbcd9ff-c994-49ac-87e7-08a93a9c0794"; configuration["SendX5C"] = "true"; configuration["ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName"; @@ -187,7 +187,7 @@ private static async Task GetAuthorizationHeaderToCallTheSideCarAsync() IServiceProvider serviceProvider = services.BuildServiceProvider(); IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); - string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("api://d05619c9-dbf2-4e60-95fd-cc75dd0db451/.default", + string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("api://aab5089d-e764-47e3-9f28-cc11c2513821/.default", new AuthorizationHeaderProviderOptions() { AcquireTokenOptions = new AcquireTokenOptions() From d0506229434bcfa2eaedd446fb1a7e4b165728b9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:00:54 -0800 Subject: [PATCH 07/12] Fix ForAgentIdentity hardcoded 'AzureAd' ConfigurationSection to respect AuthenticationOptionsName (#3635) * Initial plan * Fix ForAgentIdentity to use AuthenticationOptionsName instead of hardcoded "AzureAd" Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Fix ForAgentIdentity to use AuthenticationOptionsName instead of hardcoded "AzureAd" Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur --- .../AgentIdentitiesExtension.cs | 7 +- .../AgentIdentitiesExtensionTests.cs | 145 ++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs diff --git a/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs b/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs index 6d3c42f2d..a73f1bfe0 100644 --- a/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs +++ b/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs @@ -117,8 +117,9 @@ internal static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions op // Until it makes it way through Abstractions options.ExtraParameters[Constants.FmiPathForClientAssertion] = agentApplicationId; - // TODO: do we want to expose a mechanism to override the MicrosoftIdentityOptions instead of leveraging - // the default configuration section / named options?. + // Use the developer's AuthenticationOptionsName if set, otherwise default to "AzureAd" + string configurationSection = options.AuthenticationOptionsName ?? "AzureAd"; + options.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] = new MicrosoftEntraApplicationOptions { ClientId = agentApplicationId, // Agent identity Client ID. @@ -126,7 +127,7 @@ internal static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions op SourceType = CredentialSource.CustomSignedAssertion, CustomSignedAssertionProviderName = "OidcIdpSignedAssertion", CustomSignedAssertionProviderData = new Dictionary { - { "ConfigurationSection", "AzureAd" }, // Use the default configuration section name + { "ConfigurationSection", configurationSection }, // Use the developer's choice or default to "AzureAd" { "RequiresSignedAssertionFmiPath", true }, // The OidcIdpSignedAssertionProvider will require the fmiPath to be provided in the assertionRequestOptions. } }] diff --git a/tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs b/tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs new file mode 100644 index 000000000..8987575e2 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Identity.Abstractions; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + /// + /// Tests for the AgentIdentityExtension class (ForAgentIdentity and WithAgentIdentity methods). + /// + public class AgentIdentitiesExtensionTests + { + private const string TestAgentApplicationId = "test-agent-app-id"; + + [Fact] + public void WithAgentIdentity_WithDefaultAuthenticationOptionsName_UsesAzureAdConfigurationSection() + { + // Arrange + var options = new AuthorizationHeaderProviderOptions(); + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + Assert.NotNull(options.AcquireTokenOptions); + Assert.NotNull(options.AcquireTokenOptions.ExtraParameters); + Assert.True(options.AcquireTokenOptions.ExtraParameters.ContainsKey(Constants.MicrosoftIdentityOptionsParameter)); + + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + Assert.Equal(TestAgentApplicationId, microsoftIdentityOptions.ClientId); + + // Verify the ConfigurationSection is set to "AzureAd" when AuthenticationOptionsName is not set + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.Equal(CredentialSource.CustomSignedAssertion, clientCredential.SourceType); + Assert.Equal("OidcIdpSignedAssertion", clientCredential.CustomSignedAssertionProviderName); + Assert.NotNull(clientCredential.CustomSignedAssertionProviderData); + Assert.True(clientCredential.CustomSignedAssertionProviderData.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal("AzureAd", configSection); + } + + [Fact] + public void WithAgentIdentity_WithCustomAuthenticationOptionsName_UsesCustomConfigurationSection() + { + // Arrange + var options = new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = "MyEntraId" + } + }; + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + Assert.NotNull(options.AcquireTokenOptions); + Assert.NotNull(options.AcquireTokenOptions.ExtraParameters); + Assert.True(options.AcquireTokenOptions.ExtraParameters.ContainsKey(Constants.MicrosoftIdentityOptionsParameter)); + + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + + // Verify the ConfigurationSection respects the custom AuthenticationOptionsName + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.NotNull(clientCredential.CustomSignedAssertionProviderData); + Assert.True(clientCredential.CustomSignedAssertionProviderData.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal("MyEntraId", configSection); + } + + [Theory] + [InlineData("EntraId")] + [InlineData("CustomSection")] + [InlineData("AzureAD_Prod")] + public void WithAgentIdentity_WithVariousCustomAuthenticationOptionsNames_UsesCorrectConfigurationSection(string authenticationOptionsName) + { + // Arrange + var options = new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = authenticationOptionsName + } + }; + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters![Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.True(clientCredential.CustomSignedAssertionProviderData!.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal(authenticationOptionsName, configSection); + } + + [Fact] + public void WithAgentIdentity_WithNullOptions_CreatesNewOptionsAndUsesAzureAdDefault() + { + // Arrange + AuthorizationHeaderProviderOptions? options = null; + + // Act + var result = options!.WithAgentIdentity(TestAgentApplicationId); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.AcquireTokenOptions); + Assert.NotNull(result.AcquireTokenOptions.ExtraParameters); + + var microsoftIdentityOptions = result.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.True(clientCredential.CustomSignedAssertionProviderData!.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal("AzureAd", configSection); + } + + [Fact] + public void WithAgentIdentity_AlwaysSetsRequiresSignedAssertionFmiPath() + { + // Arrange + var options = new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = "CustomSection" + } + }; + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters![Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + var clientCredential = Assert.Single(microsoftIdentityOptions!.ClientCredentials!); + Assert.True(clientCredential.CustomSignedAssertionProviderData!.TryGetValue("RequiresSignedAssertionFmiPath", out var fmiPathRequired)); + Assert.Equal(true, fmiPathRequired); + } + } +} From 4cbb58a95835943a341bbc4058e339b1f0fe8393 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:15:03 -0800 Subject: [PATCH 08/12] Fix GetTokenAcquirer to propagate MicrosoftEntraApplicationOptions properties (#3651) * Initial plan * Fix GetTokenAcquirer to propagate MicrosoftEntraApplicationOptions properties Co-authored-by: pmaytak <34331512+pmaytak@users.noreply.github.com> * Rename variables to camelCase and copy Authority property from MicrosoftEntraApplicationOptions Co-authored-by: pmaytak <34331512+pmaytak@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pmaytak <34331512+pmaytak@users.noreply.github.com> --- ...faultTokenAcquirerFactoryImplementation.cs | 25 ++- ...TokenAcquirerFactoryImplementationTests.cs | 165 ++++++++++++++++++ 2 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs index c4c76af60..cfb4fc57b 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs @@ -59,10 +59,10 @@ public ITokenAcquirer GetTokenAcquirer(IdentityApplicationOptions IdentityApplic _ = Throws.IfNull(IdentityApplicationOptions); // Compute the Azure region if the option is a MicrosoftIdentityApplicationOptions. - MicrosoftIdentityApplicationOptions? MicrosoftIdentityApplicationOptions = IdentityApplicationOptions as MicrosoftIdentityApplicationOptions; - if (MicrosoftIdentityApplicationOptions == null) + MicrosoftIdentityApplicationOptions? microsoftIdentityApplicationOptions = IdentityApplicationOptions as MicrosoftIdentityApplicationOptions; + if (microsoftIdentityApplicationOptions == null) { - MicrosoftIdentityApplicationOptions = new MicrosoftIdentityApplicationOptions + microsoftIdentityApplicationOptions = new MicrosoftIdentityApplicationOptions { AllowWebApiToBeAuthorizedByACL = IdentityApplicationOptions.AllowWebApiToBeAuthorizedByACL, Audience = IdentityApplicationOptions.Audience, @@ -73,9 +73,24 @@ public ITokenAcquirer GetTokenAcquirer(IdentityApplicationOptions IdentityApplic TokenDecryptionCredentials = IdentityApplicationOptions.TokenDecryptionCredentials, EnablePiiLogging = IdentityApplicationOptions.EnablePiiLogging, }; + + // If the IdentityApplicationOptions is of type MicrosoftEntraApplicationOptions, + // copy over those options too. + MicrosoftEntraApplicationOptions? microsoftEntraApplicationOptions = IdentityApplicationOptions as MicrosoftEntraApplicationOptions; + if (microsoftEntraApplicationOptions != null) + { + microsoftIdentityApplicationOptions.Authority = microsoftEntraApplicationOptions.Authority; + microsoftIdentityApplicationOptions.Name = microsoftEntraApplicationOptions.Name; + microsoftIdentityApplicationOptions.Instance = microsoftEntraApplicationOptions.Instance; + microsoftIdentityApplicationOptions.TenantId = microsoftEntraApplicationOptions.TenantId; + microsoftIdentityApplicationOptions.AppHomeTenantId = microsoftEntraApplicationOptions.AppHomeTenantId; + microsoftIdentityApplicationOptions.AzureRegion = microsoftEntraApplicationOptions.AzureRegion; + microsoftIdentityApplicationOptions.ClientCapabilities = microsoftEntraApplicationOptions.ClientCapabilities; + microsoftIdentityApplicationOptions.SendX5C = microsoftEntraApplicationOptions.SendX5C; + } } - string key = GetKey(IdentityApplicationOptions.Authority, IdentityApplicationOptions.ClientId, MicrosoftIdentityApplicationOptions.AzureRegion); + string key = GetKey(IdentityApplicationOptions.Authority, IdentityApplicationOptions.ClientId, microsoftIdentityApplicationOptions.AzureRegion); return _authSchemes.GetOrAdd(key, (key) => { @@ -83,7 +98,7 @@ public ITokenAcquirer GetTokenAcquirer(IdentityApplicationOptions IdentityApplic MergedOptions mergedOptions = optionsMonitor.Get(key); - MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(MicrosoftIdentityApplicationOptions, mergedOptions); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(microsoftIdentityApplicationOptions, mergedOptions); return MakeTokenAcquirer(key); }); } diff --git a/tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs b/tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs new file mode 100644 index 000000000..7fb1b0dc7 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web.Test.Common; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class DefaultTokenAcquirerFactoryImplementationTests + { + [Fact] + public void GetTokenAcquirer_WithMicrosoftEntraApplicationOptions_PropagatesAllOptions() + { + // Arrange + var taf = new CustomTAF(); + var provider = taf.Build(); + var factory = provider.GetRequiredService(); + + var options = new MicrosoftEntraApplicationOptions + { + // IdentityApplicationOptions properties + ClientId = "test-client-id", + Authority = "https://login.microsoftonline.com/test-tenant", + EnablePiiLogging = true, + AllowWebApiToBeAuthorizedByACL = true, + Audience = "test-audience", + ClientCredentials = new List + { + new CredentialDescription { ClientSecret = "test-secret", SourceType = CredentialSource.ClientSecret } + }, + + // MicrosoftEntraApplicationOptions properties + Name = "test-name", + Instance = "https://login.microsoftonline.com/", + TenantId = "test-tenant-id", + AppHomeTenantId = "test-home-tenant-id", + AzureRegion = "westus", + ClientCapabilities = new List { "cp1" }, + SendX5C = true + }; + + // Act + var tokenAcquirer = factory.GetTokenAcquirer(options); + + // Assert + Assert.NotNull(tokenAcquirer); + + // Verify the options were properly stored in the merged options + var mergedOptionsStore = provider.GetRequiredService(); + var key = DefaultTokenAcquirerFactoryImplementation.GetKey(options.Authority, options.ClientId, options.AzureRegion); + var mergedOptions = mergedOptionsStore.Get(key); + + Assert.Equal(options.ClientId, mergedOptions.ClientId); + Assert.Equal(options.EnablePiiLogging, mergedOptions.EnablePiiLogging); + Assert.Equal(options.AllowWebApiToBeAuthorizedByACL, mergedOptions.AllowWebApiToBeAuthorizedByACL); + Assert.Equal(options.Instance, mergedOptions.Instance); + Assert.Equal(options.TenantId, mergedOptions.TenantId); + Assert.Equal(options.AppHomeTenantId, mergedOptions.AppHomeTenantId); + Assert.Equal(options.AzureRegion, mergedOptions.AzureRegion); + Assert.Equal(options.ClientCapabilities, mergedOptions.ClientCapabilities); + Assert.Equal(options.SendX5C, mergedOptions.SendX5C); + } + + [Fact] + public void GetTokenAcquirer_WithIdentityApplicationOptions_PropagatesBaseOptions() + { + // Arrange + var taf = new CustomTAF(); + var provider = taf.Build(); + var factory = provider.GetRequiredService(); + + var options = new IdentityApplicationOptions + { + ClientId = "test-client-id", + Authority = "https://login.microsoftonline.com/test-tenant", + EnablePiiLogging = true, + AllowWebApiToBeAuthorizedByACL = true, + Audience = "test-audience", + ClientCredentials = new List + { + new CredentialDescription { ClientSecret = "test-secret", SourceType = CredentialSource.ClientSecret } + } + }; + + // Act + var tokenAcquirer = factory.GetTokenAcquirer(options); + + // Assert + Assert.NotNull(tokenAcquirer); + + // Verify the options were properly stored in the merged options + var mergedOptionsStore = provider.GetRequiredService(); + var key = DefaultTokenAcquirerFactoryImplementation.GetKey(options.Authority, options.ClientId, null); + var mergedOptions = mergedOptionsStore.Get(key); + + Assert.Equal(options.ClientId, mergedOptions.ClientId); + Assert.Equal(options.EnablePiiLogging, mergedOptions.EnablePiiLogging); + Assert.Equal(options.AllowWebApiToBeAuthorizedByACL, mergedOptions.AllowWebApiToBeAuthorizedByACL); + } + + [Fact] + public void GetTokenAcquirer_WithMicrosoftIdentityApplicationOptions_UsesAsIs() + { + // Arrange + var taf = new CustomTAF(); + var provider = taf.Build(); + var factory = provider.GetRequiredService(); + + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = "test-client-id", + Authority = "https://login.microsoftonline.com/test-tenant", + EnablePiiLogging = true, + AllowWebApiToBeAuthorizedByACL = true, + Instance = "https://login.microsoftonline.com/", + TenantId = "test-tenant-id", + AzureRegion = "westus", + SendX5C = true, + Domain = "test-domain.com", + SignUpSignInPolicyId = "B2C_1_signupsignin" + }; + + // Act + var tokenAcquirer = factory.GetTokenAcquirer(options); + + // Assert + Assert.NotNull(tokenAcquirer); + + // Verify the options were properly stored in the merged options + var mergedOptionsStore = provider.GetRequiredService(); + var key = DefaultTokenAcquirerFactoryImplementation.GetKey(options.Authority, options.ClientId, options.AzureRegion); + var mergedOptions = mergedOptionsStore.Get(key); + + Assert.Equal(options.ClientId, mergedOptions.ClientId); + Assert.Equal(options.EnablePiiLogging, mergedOptions.EnablePiiLogging); + Assert.Equal(options.Instance, mergedOptions.Instance); + Assert.Equal(options.TenantId, mergedOptions.TenantId); + Assert.Equal(options.AzureRegion, mergedOptions.AzureRegion); + Assert.Equal(options.SendX5C, mergedOptions.SendX5C); + Assert.Equal(options.Domain, mergedOptions.Domain); + Assert.Equal(options.SignUpSignInPolicyId, mergedOptions.SignUpSignInPolicyId); + } + + private class CustomTAF : TokenAcquirerFactory + { + public CustomTAF() + { + this.Services.AddTokenAcquisition(); + this.Services.AddHttpClient(); + this.Services.AddSingleton(); + } + + protected override string DefineConfiguration(IConfigurationBuilder builder) + { + return AppContext.BaseDirectory; + } + } + } +} From 697d10fe443be1db855cdc66f76df943aff8b70a Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:02:04 -0800 Subject: [PATCH 09/12] Add CAE claims support for FIC + Managed Identity (#3647) * revoke * Make MSI client assertion HttpClient injection test internal * make it internal --- .../InternalAPI.Unshipped.txt | 4 + .../ManagedIdentityClientAssertion.cs | 57 +++- .../ManagedIdentityClientAssertionTestHook.cs | 19 ++ ...MicrosoftIdentityIssuerValidatorFactory.cs | 2 +- .../FederatedIdentityCaeTests.cs | 273 ++++++++++++++++++ 5 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs create mode 100644 tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs diff --git a/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Unshipped.txt index 7dc5c5811..ccf3cc459 100644 --- a/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.Identity.Web.ManagedIdentityClientAssertion.ManagedIdentityClientAssertion(string? managedIdentityClientId, string? tokenExchangeUrl, Microsoft.Extensions.Logging.ILogger? logger, Microsoft.Identity.Client.IMsalHttpClientFactory? testHttpClientFactory) -> void +Microsoft.Identity.Web.TestOnly.ManagedIdentityClientAssertionTestHook +static Microsoft.Identity.Web.TestOnly.ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests.get -> Microsoft.Identity.Client.IMsalHttpClientFactory? +static Microsoft.Identity.Web.TestOnly.ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests.set -> void diff --git a/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs index 0f9568a47..a049e3499 100644 --- a/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Web.Certificateless; +using Microsoft.Identity.Web.TestOnly; namespace Microsoft.Identity.Web { @@ -17,7 +18,7 @@ namespace Microsoft.Identity.Web /// public class ManagedIdentityClientAssertion : ClientAssertionProviderBase { - IManagedIdentityApplication _managedIdentityApplication; + private IManagedIdentityApplication _managedIdentityApplication; private readonly string _tokenExchangeUrl; private readonly ILogger? _logger; @@ -49,7 +50,33 @@ public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? t /// Optional audience of the token to be requested from Managed Identity. Default value is "api://AzureADTokenExchange". /// This value is different on clouds other than Azure Public /// A logger - public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? tokenExchangeUrl, ILogger? logger) + public ManagedIdentityClientAssertion( + string? managedIdentityClientId, + string? tokenExchangeUrl, + ILogger? logger) + : this( + managedIdentityClientId, + tokenExchangeUrl, + logger, + ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests) + { + } + + + /// + /// Same as , + /// but allows injecting a custom MSAL HttpClient factory (used by tests). + /// + /// Optional ClientId of the Managed Identity + /// Optional audience of the token to be requested from Managed Identity. Default value is "api://AzureADTokenExchange". + /// This value is different on clouds other than Azure Public + /// A logger. + /// Optional MSAL HttpClient factory. + internal ManagedIdentityClientAssertion( + string? managedIdentityClientId, + string? tokenExchangeUrl, + ILogger? logger, + IMsalHttpClientFactory? testHttpClientFactory) { _tokenExchangeUrl = tokenExchangeUrl ?? CertificatelessConstants.DefaultTokenExchangeUrl; _logger = logger; @@ -61,6 +88,12 @@ public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? t } var builder = ManagedIdentityApplicationBuilder.Create(id); + + if (testHttpClientFactory != null) + { + builder = builder.WithHttpClientFactory(testHttpClientFactory); + } + if (_logger != null) { builder = builder.WithLogging(Log, ConvertMicrosoftExtensionsLogLevelToMsal(_logger), enablePiiLogging: false); @@ -76,10 +109,24 @@ public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? t /// acquired with managed identity (certificateless). /// /// The signed assertion. - protected override async Task GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions) + protected override async Task GetClientAssertionAsync( + AssertionRequestOptions? assertionRequestOptions) { - var result = await _managedIdentityApplication - .AcquireTokenForManagedIdentity(_tokenExchangeUrl) + // Start the MI token request for the token-exchange audience + var miBuilder = _managedIdentityApplication + .AcquireTokenForManagedIdentity(_tokenExchangeUrl); + + if (assertionRequestOptions is not null) + { + // Propagate claims into the MI token request. + // This also forces MSAL to bypass the MI token cache when claims are present. + if (!string.IsNullOrEmpty(assertionRequestOptions.Claims)) + { + miBuilder.WithClaims(assertionRequestOptions.Claims); + } + } + + var result = await miBuilder .ExecuteAsync(assertionRequestOptions?.CancellationToken ?? CancellationToken.None) .ConfigureAwait(false); diff --git a/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs new file mode 100644 index 000000000..cd314e5a5 --- /dev/null +++ b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client; + +namespace Microsoft.Identity.Web.TestOnly +{ + /// + /// TEST-ONLY hook so unit tests can override the HttpClient factory used by + /// ManagedIdentityClientAssertion. + /// + internal static class ManagedIdentityClientAssertionTestHook + { + /// + /// Gets or sets the used by ManagedIdentityClientAssertion for unit testing purposes. + /// + internal static IMsalHttpClientFactory? HttpClientFactoryForTests { get; set; } + } +} diff --git a/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs b/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs index 25b1491dc..4e8d680e1 100644 --- a/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs +++ b/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs @@ -17,7 +17,7 @@ public class MicrosoftIdentityIssuerValidatorFactory /// Initializes a new instance of the class. /// /// Options passed-in to create the AadIssuerValidator object. - /// HttpClientFactory. + /// HttpClientFactoryForTests. public MicrosoftIdentityIssuerValidatorFactory( IOptions aadIssuerValidatorOptions, IHttpClientFactory httpClientFactory) diff --git a/tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs b/tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs new file mode 100644 index 000000000..9542b2436 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.Test; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.Test.Common.Mocks; +using Microsoft.Identity.Web.TestOnly; +using Xunit; + +namespace Microsoft.Identity.Web.Tests.Certificateless +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class FederatedIdentityCaeTests + { + private const string Scope = "https://graph.microsoft.com/.default"; + private const string CaeClaims = @"{""access_token"":{""xms_cc"":{""values"":[""claims1""]}}}"; + private const string UamiClientId = "04ca4d6a-c720-4ba1-aa06-f6634b73fe7a"; + + [Fact] + public async Task ManagedIdentityWithFic_WithClaims_BypassesCache() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var factory = TokenAcquirerFactory.GetDefaultInstance(); + + // Configure FIC via Managed Identity–signed assertion + factory.Services.Configure(opts => + { + opts.Instance = "https://login.microsoftonline.com/"; + opts.TenantId = "11111111-1111-1111-1111-111111111111"; + opts.ClientId = "00000000-0000-0000-0000-000000000000"; + + // Make this a CP1-capable app + opts.ClientCapabilities = new[] { "cp1" }; + opts.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity, + ManagedIdentityClientId = UamiClientId + } + }; + }); + + // Mock IMDS (for the MI assertion) + var mockMiHttp = new MockHttpClientFactory(); + mockMiHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("mi-assertion-token")); + + var miTestFactory = new TestManagedIdentityHttpFactory(mockMiHttp); + ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests = miTestFactory.Create(); + factory.Services.AddSingleton(_ => miTestFactory); + + // Mock AAD token responses for client credentials + var mockMsalHttp = new MockHttpClientFactory(); + + // 1st HTTP call to AAD -> token1 + var firstTokenHandler = mockMsalHttp.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("token1")); + + // 2nd HTTP call to AAD -> token2 + var secondTokenHandler = mockMsalHttp.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("token2")); + + factory.Services.AddSingleton(_ => mockMsalHttp); + + var acquirer = factory.Build().GetRequiredService(); + + // ---------- 1) First call – no custom claims, must hit IdP ---------- + var r1 = await acquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions()); + + Assert.Equal("token1", r1.AccessToken); + Assert.Equal(TokenSource.IdentityProvider, r1.AuthenticationResultMetadata.TokenSource); + + // First HTTP request already contains CP1 in xms_cc because of ClientCapabilities + Assert.True(firstTokenHandler.ActualRequestPostData.TryGetValue("claims", out var firstClaimsJson)); + using (var doc = JsonDocument.Parse(firstClaimsJson)) + { + var values = doc.RootElement + .GetProperty("access_token") + .GetProperty("xms_cc") + .GetProperty("values"); + + bool hasCp1 = false; + bool hasClaims1 = false; + + foreach (var v in values.EnumerateArray()) + { + var s = v.GetString(); + if (s == "cp1") hasCp1 = true; + if (s == "claims1") hasClaims1 = true; + } + + Assert.True(hasCp1); // capability propagated + Assert.False(hasClaims1); // custom CAE claim NOT present yet + } + + // ---------- 2) Second call – still no claims, should come from CACHE ---------- + var r2 = await acquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions()); + + // Same token as r1 and came from cache (no extra HTTP call to AAD) + Assert.Equal("token1", r2.AccessToken); + Assert.Equal(TokenSource.Cache, r2.AuthenticationResultMetadata.TokenSource); + + // ---------- 3) Third call – WITH claims, must bypass cache and hit IdP ---------- + var r3 = await acquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions { Claims = CaeClaims }); + + // New token & explicitly from IdentityProvider + Assert.Equal("token2", r3.AccessToken); + Assert.Equal(TokenSource.IdentityProvider, r3.AuthenticationResultMetadata.TokenSource); + + // And the actual HTTP POST for that second AAD call contained the merged claims (cp1 + mark1) + Assert.True(secondTokenHandler.ActualRequestPostData.TryGetValue("claims", out var secondClaimsJson)); + + using (var doc = JsonDocument.Parse(secondClaimsJson)) + { + var values = doc.RootElement + .GetProperty("access_token") + .GetProperty("xms_cc") + .GetProperty("values"); + + bool hasCp1 = false; + bool hasClaims1 = false; + + foreach (var v in values.EnumerateArray()) + { + var s = v.GetString(); + if (s == "cp1") hasCp1 = true; + if (s == "claims1") hasClaims1 = true; + } + + Assert.True(hasCp1); // capability kept + Assert.True(hasClaims1); // custom CAE claim merged in + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Fic_CustomSignedAssertion_ClaimsAndCapabilities_AreSent_OnSecondRequest(bool withFmiPath) + { + using var httpFactoryForTest = new MockHttpClientFactory(); + // First request (credential exchange) + var credentialRequestHttpHandler = httpFactoryForTest.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("token-exchange-1")); + // Second request (actual token acquisition) + var tokenRequestHttpHandler = httpFactoryForTest.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("final-access-token")); + + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.AddOidcFic(); + tokenAcquirerFactory.Services.AddSingleton(httpFactoryForTest); + + // Source app (provides assertion) + tokenAcquirerFactory.Services.Configure("AzureAd2", options => + { + options.Instance = "https://login.microsoftonline.us/"; + options.TenantId = "t1"; + options.ClientId = "c1"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = TestConstants.ClientSecret + } + }; + }); + + // Target app (uses custom signed assertion and carries cp1) + var customAssertionProvidedData = new Dictionary + { + ["ConfigurationSection"] = "AzureAd2" + }; + if (withFmiPath) + { + customAssertionProvidedData["RequiresSignedAssertionFmiPath"] = true; + } + + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "t2"; + options.ClientId = "c2"; + options.ClientCapabilities = new[] { "cp1" }; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.CustomSignedAssertion, + CustomSignedAssertionProviderName = "OidcIdpSignedAssertion", + CustomSignedAssertionProviderData = customAssertionProvidedData + } + }; + }); + + var serviceProvider = tokenAcquirerFactory.Build(); + var authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + // Second call will carry claims (with cp1 in xms_cc) + var claimsPayload = "{\"access_token\":{\"xms_cc\":{\"values\":[\"cp1\"]}}}"; + + var header = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync( + TestConstants.s_scopeForApp, + new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + Claims = claimsPayload, + ExtraParameters = withFmiPath + ? new Dictionary + { + [Constants.FmiPathForClientAssertion] = "myFmiPathForSignedAssertion" + } + : null + } + }); + + // Assert endpoints, scopes, client IDs + Assert.Equal("api://AzureADTokenExchange/.default", credentialRequestHttpHandler.ActualRequestPostData["scope"]); + Assert.Equal(TestConstants.s_scopeForApp, tokenRequestHttpHandler.ActualRequestPostData["scope"]); + Assert.Equal("c1", credentialRequestHttpHandler.ActualRequestPostData["client_id"]); + Assert.Equal("https://login.microsoftonline.us/t1/oauth2/v2.0/token", + credentialRequestHttpHandler.ActualRequestMessage?.RequestUri?.AbsoluteUri); + Assert.Equal("c2", tokenRequestHttpHandler.ActualRequestPostData["client_id"]); + Assert.Equal("https://login.microsoftonline.com/t2/oauth2/v2.0/token", + tokenRequestHttpHandler.ActualRequestMessage?.RequestUri?.AbsoluteUri); + + if (withFmiPath) + { + Assert.Equal("myFmiPathForSignedAssertion", credentialRequestHttpHandler.ActualRequestPostData["fmi_path"]); + } + + // Claims: absent on first request, present on second with cp1 + Assert.False(credentialRequestHttpHandler.ActualRequestPostData.ContainsKey("claims")); + Assert.True(tokenRequestHttpHandler.ActualRequestPostData.ContainsKey("claims")); + + var claimsJson = tokenRequestHttpHandler.ActualRequestPostData["claims"]; + using var doc = JsonDocument.Parse(claimsJson); + var cp = doc.RootElement + .GetProperty("access_token") + .GetProperty("xms_cc") + .GetProperty("values")[0] + .GetString(); + Assert.Equal("cp1", cp); + + // First token is reused as client_assertion on second request + string accessTokenFromRequest1; + using (var document = JsonDocument.Parse(credentialRequestHttpHandler.ResponseString)) + { + accessTokenFromRequest1 = document.RootElement.GetProperty("access_token").GetString()!; + } + Assert.Equal(accessTokenFromRequest1, tokenRequestHttpHandler.ActualRequestPostData["client_assertion"]); + + // Bearer header returned + Assert.StartsWith("Bearer", header, StringComparison.Ordinal); + } + } +} From bdf69b3ef3413b42ce6b9f8360f589d19c9d2790 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:06:10 -0800 Subject: [PATCH 10/12] Add AddMicrosoftIdentityMessageHandler extension methods for IHttpClientBuilder (#3649) * Initial plan * Add MicrosoftIdentityHttpClientBuilderExtensions with 4 overloads and unit tests Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Add comprehensive documentation for AddMicrosoftIdentityMessageHandler extension methods Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Update expectedWarningCount to 52 in test-aot.ps1 Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur --- build/test-aot.ps1 | 2 +- docs/calling-downstream-apis/custom-apis.md | 384 ++++++++++++++++ ...softIdentityHttpClientBuilderExtensions.cs | 419 ++++++++++++++++++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net462/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net472/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 5 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 5 + .../netstandard2.0/PublicAPI.Unshipped.txt | 5 + ...dentityHttpClientBuilderExtensionsTests.cs | 348 +++++++++++++++ 10 files changed, 1182 insertions(+), 1 deletion(-) create mode 100644 docs/calling-downstream-apis/custom-apis.md create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs create mode 100644 tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs diff --git a/build/test-aot.ps1 b/build/test-aot.ps1 index 2a59dfa57..95f3c8d32 100644 --- a/build/test-aot.ps1 +++ b/build/test-aot.ps1 @@ -23,7 +23,7 @@ foreach ($line in $($publishOutput -split "`r`n")) } Write-Host "Actual warning count is: ", $actualWarningCount -$expectedWarningCount = 50 +$expectedWarningCount = 52 if ($LastExitCode -ne 0) { diff --git a/docs/calling-downstream-apis/custom-apis.md b/docs/calling-downstream-apis/custom-apis.md new file mode 100644 index 000000000..0b0200cd9 --- /dev/null +++ b/docs/calling-downstream-apis/custom-apis.md @@ -0,0 +1,384 @@ +# Calling Custom APIs with MicrosoftIdentityMessageHandler + +This guide explains how to use `MicrosoftIdentityMessageHandler` for HttpClient integration to call custom downstream APIs with automatic Microsoft Identity authentication. + +## Table of Contents + +- [Overview](#overview) +- [MicrosoftIdentityMessageHandler - For HttpClient Integration](#microsoftidentitymessagehandler---for-httpclient-integration) + - [Before: Manual Setup](#before-manual-setup) + - [After: Using Extension Methods](#after-using-extension-methods) + - [Configuration Examples](#configuration-examples) +- [Per-Request Options](#per-request-options) +- [Advanced Scenarios](#advanced-scenarios) + +## Overview + +`MicrosoftIdentityMessageHandler` is a `DelegatingHandler` that automatically adds authorization headers to outgoing HTTP requests. The new `AddMicrosoftIdentityMessageHandler` extension methods make it easy to configure HttpClient instances with automatic Microsoft Identity authentication. + +## MicrosoftIdentityMessageHandler - For HttpClient Integration + +### Before: Manual Setup + +Previously, you had to manually configure the message handler: + +```csharp +// In Program.cs or Startup.cs +services.AddHttpClient("MyApiClient", client => +{ + client.BaseAddress = new Uri("https://api.example.com"); +}) +.AddHttpMessageHandler(serviceProvider => new MicrosoftIdentityMessageHandler( + serviceProvider.GetRequiredService(), + new MicrosoftIdentityMessageHandlerOptions + { + Scopes = { "https://api.example.com/.default" } + })); +``` + +### After: Using Extension Methods + +Now you can use the convenient extension methods: + +#### 1. Parameterless Overload (Per-Request Configuration) + +Use this when you want to configure authentication options on a per-request basis: + +```csharp +services.AddHttpClient("FlexibleClient") + .AddMicrosoftIdentityMessageHandler(); + +// Later, in a service: +var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + .WithAuthenticationOptions(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }); + +var response = await httpClient.SendAsync(request); +``` + +#### 2. Options Instance Overload + +Use this when you have a pre-configured options object: + +```csharp +var options = new MicrosoftIdentityMessageHandlerOptions +{ + Scopes = { "https://graph.microsoft.com/.default" } +}; +options.WithAgentIdentity("agent-application-id"); + +services.AddHttpClient("GraphClient", client => +{ + client.BaseAddress = new Uri("https://graph.microsoft.com"); +}) +.AddMicrosoftIdentityMessageHandler(options); +``` + +#### 3. Action Delegate Overload (Inline Configuration) + +Use this for inline configuration - the most common scenario: + +```csharp +services.AddHttpClient("MyApiClient", client => +{ + client.BaseAddress = new Uri("https://api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("https://api.example.com/.default"); + options.RequestAppToken = true; +}); +``` + +#### 4. IConfiguration Overload (Configuration from appsettings.json) + +Use this to configure from appsettings.json: + +**appsettings.json:** +```json +{ + "DownstreamApi": { + "Scopes": ["https://api.example.com/.default"] + }, + "GraphApi": { + "Scopes": ["https://graph.microsoft.com/.default", "User.Read"] + } +} +``` + +**Program.cs:** +```csharp +services.AddHttpClient("DownstreamApiClient", client => +{ + client.BaseAddress = new Uri("https://api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("DownstreamApi"), + "DownstreamApi"); + +services.AddHttpClient("GraphClient", client => +{ + client.BaseAddress = new Uri("https://graph.microsoft.com"); +}) +.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("GraphApi"), + "GraphApi"); +``` + +### Configuration Examples + +#### Example 1: Simple Web API Client + +```csharp +// Configure in Program.cs +services.AddHttpClient("WeatherApiClient", client => +{ + client.BaseAddress = new Uri("https://api.weather.com"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("https://api.weather.com/.default"); +}); + +// Use in a controller or service +public class WeatherService +{ + private readonly HttpClient _httpClient; + + public WeatherService(IHttpClientFactory factory) + { + _httpClient = factory.CreateClient("WeatherApiClient"); + } + + public async Task GetForecastAsync(string city) + { + var response = await _httpClient.GetAsync($"/forecast/{city}"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } +} +``` + +#### Example 2: Multiple API Clients + +```csharp +// Configure multiple clients in Program.cs +services.AddHttpClient("ApiClient1") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api1.example.com/.default"); + }); + +services.AddHttpClient("ApiClient2") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api2.example.com/.default"); + options.RequestAppToken = true; + }); + +// Use in a service +public class MultiApiService +{ + private readonly HttpClient _client1; + private readonly HttpClient _client2; + + public MultiApiService(IHttpClientFactory factory) + { + _client1 = factory.CreateClient("ApiClient1"); + _client2 = factory.CreateClient("ApiClient2"); + } + + public async Task GetFromBothApisAsync() + { + var data1 = await _client1.GetStringAsync("/data"); + var data2 = await _client2.GetStringAsync("/data"); + return $"{data1} | {data2}"; + } +} +``` + +#### Example 3: Configuration from appsettings.json with Complex Options + +**appsettings.json:** +```json +{ + "DownstreamApis": { + "CustomerApi": { + "Scopes": ["api://customer-api/.default"] + }, + "OrderApi": { + "Scopes": ["api://order-api/.default"] + }, + "InventoryApi": { + "Scopes": ["api://inventory-api/.default"] + } + } +} +``` + +**Program.cs:** +```csharp +var downstreamApis = configuration.GetSection("DownstreamApis"); + +services.AddHttpClient("CustomerApiClient", client => +{ + client.BaseAddress = new Uri("https://customer-api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + downstreamApis.GetSection("CustomerApi"), + "CustomerApi"); + +services.AddHttpClient("OrderApiClient", client => +{ + client.BaseAddress = new Uri("https://order-api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + downstreamApis.GetSection("OrderApi"), + "OrderApi"); + +services.AddHttpClient("InventoryApiClient", client => +{ + client.BaseAddress = new Uri("https://inventory-api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + downstreamApis.GetSection("InventoryApi"), + "InventoryApi"); +``` + +## Per-Request Options + +You can override default options on a per-request basis using the `WithAuthenticationOptions` extension method: + +```csharp +// Configure client with default options +services.AddHttpClient("ApiClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }); + +// Override for specific requests +public class MyService +{ + private readonly HttpClient _httpClient; + + public MyService(IHttpClientFactory factory) + { + _httpClient = factory.CreateClient("ApiClient"); + } + + public async Task GetSensitiveDataAsync() + { + // Override scopes for this specific request + var request = new HttpRequestMessage(HttpMethod.Get, "/api/sensitive") + .WithAuthenticationOptions(options => + { + options.Scopes.Clear(); + options.Scopes.Add("https://api.example.com/sensitive.read"); + options.RequestAppToken = true; + }); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } +} +``` + +## Advanced Scenarios + +### Agent Identity + +Use agent identity when your application needs to act on behalf of another application: + +```csharp +services.AddHttpClient("AgentClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://graph.microsoft.com/.default"); + options.WithAgentIdentity("agent-application-id"); + options.RequestAppToken = true; + }); +``` + +### Composing with Other Handlers + +You can chain multiple handlers in the pipeline: + +```csharp +services.AddHttpClient("ApiClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }) + .AddHttpMessageHandler() + .AddHttpMessageHandler(); +``` + +### WWW-Authenticate Challenge Handling + +`MicrosoftIdentityMessageHandler` automatically handles WWW-Authenticate challenges for Conditional Access scenarios: + +```csharp +// No additional code needed - automatic handling +services.AddHttpClient("ProtectedApiClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }); + +// The handler will automatically: +// 1. Detect 401 responses with WWW-Authenticate challenges +// 2. Extract required claims from the challenge +// 3. Acquire a new token with the additional claims +// 4. Retry the request with the new token +``` + +### Error Handling + +```csharp +public class MyService +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public MyService(IHttpClientFactory factory, ILogger logger) + { + _httpClient = factory.CreateClient("ApiClient"); + _logger = logger; + } + + public async Task GetDataWithErrorHandlingAsync() + { + try + { + var response = await _httpClient.GetAsync("/api/data"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + catch (MicrosoftIdentityAuthenticationException authEx) + { + _logger.LogError(authEx, "Authentication failed: {Message}", authEx.Message); + throw; + } + catch (HttpRequestException httpEx) + { + _logger.LogError(httpEx, "HTTP request failed: {Message}", httpEx.Message); + throw; + } + } +} +``` + +## Summary + +The `AddMicrosoftIdentityMessageHandler` extension methods provide a clean, flexible way to configure HttpClient with automatic Microsoft Identity authentication: + +- **Parameterless**: For per-request configuration flexibility +- **Options instance**: For pre-configured options objects +- **Action delegate**: For inline configuration (most common) +- **IConfiguration**: For configuration from appsettings.json + +Choose the overload that best fits your scenario and enjoy automatic authentication for your downstream API calls! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs new file mode 100644 index 000000000..55fccad1b --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; + +namespace Microsoft.Identity.Web +{ + /// + /// Extension methods for to add + /// to the HTTP client pipeline with various configuration options. + /// + /// + /// + /// These extension methods provide a convenient way to configure HttpClient instances with automatic + /// Microsoft Identity authentication using . The handler + /// will automatically add authorization headers to outgoing HTTP requests based on the configured options. + /// + /// + /// Four overloads are provided to support different configuration scenarios: + /// + /// + /// Parameterless: For scenarios where options are set per-request + /// Options instance: For programmatic configuration with a pre-built options object + /// Action delegate: For inline configuration using a configuration delegate + /// IConfiguration: For configuration from appsettings.json or other configuration sources + /// + /// + /// + /// Basic usage with inline configuration: + /// + /// services.AddHttpClient("MyApiClient", client => + /// { + /// client.BaseAddress = new Uri("https://api.example.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler(options => + /// { + /// options.Scopes.Add("https://api.example.com/.default"); + /// }); + /// + /// + /// Configuration from appsettings.json: + /// + /// // In appsettings.json: + /// // { + /// // "DownstreamApi": { + /// // "Scopes": ["https://graph.microsoft.com/.default"] + /// // } + /// // } + /// + /// services.AddHttpClient("GraphClient") + /// .AddMicrosoftIdentityMessageHandler( + /// configuration.GetSection("DownstreamApi"), + /// "DownstreamApi"); + /// + /// + /// Parameterless for per-request configuration: + /// + /// services.AddHttpClient("FlexibleClient") + /// .AddMicrosoftIdentityMessageHandler(); + /// + /// // Later, in a service: + /// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + /// .WithAuthenticationOptions(options => + /// { + /// options.Scopes.Add("custom.scope"); + /// }); + /// var response = await httpClient.SendAsync(request); + /// + /// + /// + /// + /// + public static class MicrosoftIdentityHttpClientBuilderExtensions + { + /// + /// Adds a to the HTTP client pipeline with no default options. + /// Options must be configured per-request using . + /// + /// The to configure. + /// The for method chaining. + /// Thrown when is . + /// + /// + /// This overload is useful when you need maximum flexibility to configure authentication options + /// on a per-request basis. Since no default options are provided, every request must include + /// authentication options via the WithAuthenticationOptions extension method. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// + /// // Configure the HTTP client + /// services.AddHttpClient("ApiClient") + /// .AddMicrosoftIdentityMessageHandler(); + /// + /// // Use the client with per-request configuration + /// public class MyService + /// { + /// private readonly HttpClient _httpClient; + /// + /// public MyService(IHttpClientFactory factory) + /// { + /// _httpClient = factory.CreateClient("ApiClient"); + /// } + /// + /// public async Task<string> GetDataAsync() + /// { + /// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + /// .WithAuthenticationOptions(options => + /// { + /// options.Scopes.Add("https://api.example.com/.default"); + /// }); + /// + /// var response = await _httpClient.SendAsync(request); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddHttpMessageHandler(sp => + { + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider); + }); + } + + /// + /// Adds a to the HTTP client pipeline with the specified options. + /// + /// The to configure. + /// The authentication options to use for all requests made by this client. + /// The for method chaining. + /// + /// Thrown when or is . + /// + /// + /// + /// This overload is useful when you have a pre-configured + /// instance that should be used for all requests made by this HTTP client. Individual requests can still + /// override these default options using the per-request extension methods. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// + /// // Pre-configure options + /// var options = new MicrosoftIdentityMessageHandlerOptions + /// { + /// Scopes = { "https://graph.microsoft.com/.default" } + /// }; + /// options.WithAgentIdentity("agent-application-id"); + /// + /// // Configure the HTTP client with the pre-built options + /// services.AddHttpClient("GraphClient", client => + /// { + /// client.BaseAddress = new Uri("https://graph.microsoft.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler(options); + /// + /// // Use the client - authentication is automatic + /// public class GraphService + /// { + /// private readonly HttpClient _httpClient; + /// + /// public GraphService(IHttpClientFactory factory) + /// { + /// _httpClient = factory.CreateClient("GraphClient"); + /// } + /// + /// public async Task<string> GetUserProfileAsync() + /// { + /// var response = await _httpClient.GetAsync("/v1.0/me"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder, + MicrosoftIdentityMessageHandlerOptions options) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return builder.AddHttpMessageHandler(sp => + { + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider, options); + }); + } + + /// + /// Adds a to the HTTP client pipeline with options configured via delegate. + /// + /// The to configure. + /// A delegate to configure the authentication options. + /// The for method chaining. + /// + /// Thrown when or is . + /// + /// + /// + /// This overload is useful for inline configuration of authentication options. The delegate is called + /// once during service configuration to create the default options for the HTTP client. + /// Individual requests can still override these default options using the per-request extension methods. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// + /// // Configure the HTTP client with inline options configuration + /// services.AddHttpClient("MyApiClient", client => + /// { + /// client.BaseAddress = new Uri("https://api.example.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler(options => + /// { + /// options.Scopes.Add("https://api.example.com/.default"); + /// options.RequestAppToken = true; + /// }); + /// + /// // Use the client - authentication is automatic + /// public class ApiService + /// { + /// private readonly HttpClient _httpClient; + /// + /// public ApiService(IHttpClientFactory factory) + /// { + /// _httpClient = factory.CreateClient("MyApiClient"); + /// } + /// + /// public async Task<string> GetDataAsync() + /// { + /// var response = await _httpClient.GetAsync("/api/data"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + /// With agent identity: + /// + /// services.AddHttpClient("AgentClient") + /// .AddMicrosoftIdentityMessageHandler(options => + /// { + /// options.Scopes.Add("https://graph.microsoft.com/.default"); + /// options.WithAgentIdentity("agent-application-id"); + /// options.RequestAppToken = true; + /// }); + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder, + Action configureOptions) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + return builder.AddHttpMessageHandler(sp => + { + var options = new MicrosoftIdentityMessageHandlerOptions(); + configureOptions(options); + + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider, options); + }); + } + + /// + /// Adds a to the HTTP client pipeline with options bound from IConfiguration. + /// + /// The to configure. + /// The configuration section containing the authentication options. + /// The name of the configuration section (used for diagnostics). + /// The for method chaining. + /// + /// Thrown when , , or is . + /// + /// + /// + /// This overload is useful when you want to configure authentication options from appsettings.json + /// or other configuration sources. The configuration section is bound to a new + /// instance using standard configuration binding. + /// Individual requests can still override these default options using the per-request extension methods. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// Configuration in appsettings.json: + /// + /// { + /// "DownstreamApi": { + /// "Scopes": ["https://api.example.com/.default"] + /// }, + /// "GraphApi": { + /// "Scopes": ["https://graph.microsoft.com/.default", "User.Read"] + /// } + /// } + /// + /// + /// Configure the HTTP client: + /// + /// // In Program.cs or Startup.cs + /// services.AddHttpClient("DownstreamApiClient", client => + /// { + /// client.BaseAddress = new Uri("https://api.example.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler( + /// configuration.GetSection("DownstreamApi"), + /// "DownstreamApi"); + /// + /// services.AddHttpClient("GraphClient", client => + /// { + /// client.BaseAddress = new Uri("https://graph.microsoft.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler( + /// configuration.GetSection("GraphApi"), + /// "GraphApi"); + /// + /// + /// Use the clients: + /// + /// public class MyService + /// { + /// private readonly HttpClient _apiClient; + /// private readonly HttpClient _graphClient; + /// + /// public MyService(IHttpClientFactory factory) + /// { + /// _apiClient = factory.CreateClient("DownstreamApiClient"); + /// _graphClient = factory.CreateClient("GraphClient"); + /// } + /// + /// public async Task<string> GetApiDataAsync() + /// { + /// var response = await _apiClient.GetAsync("/api/data"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// + /// public async Task<string> GetUserProfileAsync() + /// { + /// var response = await _graphClient.GetAsync("/v1.0/me"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder, + IConfiguration configuration, + string sectionName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (sectionName == null) + { + throw new ArgumentNullException(nameof(sectionName)); + } + + return builder.AddHttpMessageHandler(sp => + { + var options = new MicrosoftIdentityMessageHandlerOptions(); + configuration.Bind(options); + + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider, options); + }); + } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt index ec706b293..86db2ee51 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -4,6 +4,7 @@ Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions Microsoft.Identity.Web.MicrosoftIdentityMessageHandler Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions @@ -14,3 +15,7 @@ override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt index 13496576f..cc1519cb7 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -3,6 +3,7 @@ Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions Microsoft.Identity.Web.MicrosoftIdentityMessageHandler Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions @@ -13,3 +14,7 @@ override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt index 13496576f..cc1519cb7 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -3,6 +3,7 @@ Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions Microsoft.Identity.Web.MicrosoftIdentityMessageHandler Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions @@ -13,3 +14,7 @@ override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 13496576f..cc1519cb7 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -3,6 +3,7 @@ Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions Microsoft.Identity.Web.MicrosoftIdentityMessageHandler Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions @@ -13,3 +14,7 @@ override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 13496576f..cc1519cb7 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -3,6 +3,7 @@ Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions Microsoft.Identity.Web.MicrosoftIdentityMessageHandler Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions @@ -13,3 +14,7 @@ override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 13496576f..cc1519cb7 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -3,6 +3,7 @@ Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions Microsoft.Identity.Web.MicrosoftIdentityMessageHandler Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions @@ -13,3 +14,7 @@ override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! diff --git a/tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs new file mode 100644 index 000000000..66e71db2e --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using NSubstitute; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class MicrosoftIdentityHttpClientBuilderExtensionsTests + { + private readonly IAuthorizationHeaderProvider _mockHeaderProvider; + + public MicrosoftIdentityHttpClientBuilderExtensionsTests() + { + _mockHeaderProvider = Substitute.For(); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_Parameterless_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + + // Act & Assert + Assert.Throws(() => builder!.AddMicrosoftIdentityMessageHandler()); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_Parameterless_RegistersHandler() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler(); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithOptions_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + var options = new MicrosoftIdentityMessageHandlerOptions + { + Scopes = { "test.scope" } + }; + + // Act & Assert + Assert.Throws(() => builder!.AddMicrosoftIdentityMessageHandler(options)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithOptions_WithNullOptions_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + MicrosoftIdentityMessageHandlerOptions? options = null; + + // Act & Assert + Assert.Throws(() => builder.AddMicrosoftIdentityMessageHandler(options!)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithOptions_RegistersHandlerWithOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + var options = new MicrosoftIdentityMessageHandlerOptions + { + Scopes = { "https://graph.microsoft.com/.default" } + }; + + // Act + builder.AddMicrosoftIdentityMessageHandler(options); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithDelegate_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + Action configureOptions = options => + { + options.Scopes.Add("test.scope"); + }; + + // Act & Assert + Assert.Throws(() => builder!.AddMicrosoftIdentityMessageHandler(configureOptions)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithDelegate_WithNullDelegate_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + Action? configureOptions = null; + + // Act & Assert + Assert.Throws(() => builder.AddMicrosoftIdentityMessageHandler(configureOptions!)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithDelegate_RegistersHandlerWithConfiguredOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + options.RequestAppToken = true; + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + var configuration = new ConfigurationBuilder().Build(); + + // Act & Assert + Assert.Throws(() => + builder!.AddMicrosoftIdentityMessageHandler(configuration, "TestSection")); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_WithNullConfiguration_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + IConfiguration? configuration = null; + + // Act & Assert + Assert.Throws(() => + builder.AddMicrosoftIdentityMessageHandler(configuration!, "TestSection")); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_WithNullSectionName_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + var configuration = new ConfigurationBuilder().Build(); + string? sectionName = null; + + // Act & Assert + Assert.Throws(() => + builder.AddMicrosoftIdentityMessageHandler(configuration, sectionName!)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_RegistersHandlerWithBoundOptions() + { + // Arrange + var configurationData = new Dictionary + { + { "DownstreamApi:Scopes:0", "https://graph.microsoft.com/.default" }, + { "DownstreamApi:Scopes:1", "User.Read" } + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configurationData) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("DownstreamApi"), + "DownstreamApi"); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_ServiceResolution_ResolvesAuthorizationHeaderProvider() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("test.scope"); + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + + // Verify that IAuthorizationHeaderProvider can be resolved + var headerProvider = serviceProvider.GetRequiredService(); + Assert.NotNull(headerProvider); + Assert.Same(_mockHeaderProvider, headerProvider); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_MultipleHandlers_CanBeChained() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act - Add multiple handlers to the pipeline + builder + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("test.scope"); + }) + .AddHttpMessageHandler(() => new TestDelegatingHandler()); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_ReturnsBuilder_AllowsChaining() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + var result = builder.AddMicrosoftIdentityMessageHandler(); + + // Assert + Assert.NotNull(result); + Assert.Same(builder, result); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_EmptyConfiguration_RegistersHandlerWithEmptyOptions() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("NonExistent"), + "NonExistent"); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_MultipleClients_CanHaveDifferentConfigurations() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + + // Act - Configure multiple clients with different options + services.AddHttpClient("Client1") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("scope1"); + }); + + services.AddHttpClient("Client2") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("scope2"); + options.RequestAppToken = true; + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + + var client1 = httpClientFactory.CreateClient("Client1"); + var client2 = httpClientFactory.CreateClient("Client2"); + + Assert.NotNull(client1); + Assert.NotNull(client2); + } + + // Test helper class + private class TestDelegatingHandler : DelegatingHandler + { + } + } +} From 775cd0bbb965b09059a6c2f2ca98c9e0de558a72 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:53:21 -0800 Subject: [PATCH 11/12] Update support policy (#3656) --- supportPolicy.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/supportPolicy.md b/supportPolicy.md index efa6556a3..d386ee16e 100644 --- a/supportPolicy.md +++ b/supportPolicy.md @@ -1,19 +1,20 @@ # Microsoft.Identity.Web Support Policy -_Last updated May 12, 2025_ +_Last updated December 16, 2025_ ## Supported versions The following table lists IdentityWeb versions currently supported and receiving security fixes. | Major Version | Last Release | Patch Release Date | Support Phase|End of Support | | --------------|--------------|--------|------------|--------| -| 3.x | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/) |Monthly| Active | Not planned.
✅Supported versions: from 3.0.0 to [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/)
⚠️Unsupported versions `< 3.0.0`.| +| 4.x | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/) |Monthly| Active | Not planned.
✅Supported versions: from 4.0.0 to [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/)
⚠️Unsupported versions `< 4.0.0`.| ## Out of support versions The following table lists Microsoft.Identity.Web versions no longer supported and no longer receiving security fixes. | Major Version | Latest Patch Version| Patch Release Date | End of Support Date| | --------------|--------------|--------|--------| +| 3.x | 3.14.1 | August 27, 2025 | October 13, 2025 | | 2.x | 2.21.0 | July 18, 2024 | January 1, 2025 | | 1.x | 1.26.0 | February 5, 2023 | February 5, 2023| From 1a0036d9d8884ca3c16ca81726962bb19648648e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:17:55 -0800 Subject: [PATCH 12/12] Prepare version 4.2.0 release (#3655) * Initial plan * Update changelog for version 4.2.0 Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Update version to 4.2.0 and Microsoft.Identity.Abstractions to 10.0.0 Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Add support policy update to changelog fundamentals section Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur --- Directory.Build.props | 4 ++-- changelog.md | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e97935ed8..1113beee0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 4.1.1 + 4.2.0 $(MicrosoftIdentityWebVersion) @@ -84,7 +84,7 @@ 8.15.0 4.79.2 - 9.6.0 + 10.0.0 3.3.0 4.7.2 4.6.0 diff --git a/changelog.md b/changelog.md index 581f782ce..1771daf78 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,25 @@ +## 4.2.0 + +### New features +- **Added CAE claims support for FIC + Managed Identity**. See [#3647](https://github.com/AzureAD/microsoft-identity-web/pull/3647) for details. +- **Added `AddMicrosoftIdentityMessageHandler` extension methods for `IHttpClientBuilder`**. See [#3649](https://github.com/AzureAD/microsoft-identity-web/pull/3649) for details. + +### Bug fixes +- **Fixed tenant not being propagated in credential FIC acquisition**. See [#3633](https://github.com/AzureAD/microsoft-identity-web/pull/3633) for details. +- **Fixed `ForAgentIdentity` hardcoded 'AzureAd' `ConfigurationSection` to respect `AuthenticationOptionsName`**. See [#3635](https://github.com/AzureAD/microsoft-identity-web/pull/3635) for details. +- **Fixed `GetTokenAcquirer` to propagate `MicrosoftEntraApplicationOptions` properties**. See [#3651](https://github.com/AzureAD/microsoft-identity-web/pull/3651) for details. +- **Added meaningful error message when identity configuration is missing**. See [#3637](https://github.com/AzureAD/microsoft-identity-web/pull/3637) for details. + +### Dependencies updates +- Update Microsoft.Identity.Abstractions to version 10.0.0. +- Bump express from 5.1.0 to 5.2.0 in /tests/DevApps/SidecarAdapter/typescript. [#3636](https://github.com/AzureAD/microsoft-identity-web/pull/3636) +- Bump jws from 3.2.2 to 3.2.3 in /tests/DevApps/SidecarAdapter/typescript. [#3641](https://github.com/AzureAD/microsoft-identity-web/pull/3641) + +### Fundamentals +- Update support policy. [#3656](https://github.com/AzureAD/microsoft-identity-web/pull/3656) +- Update agent identity coordinates in E2E tests after deauth. [#3640](https://github.com/AzureAD/microsoft-identity-web/pull/3640) +- Update E2E agent identity configuration to new tenant. [#3646](https://github.com/AzureAD/microsoft-identity-web/pull/3646) + ## 4.1.1 ### Bug fixes