Skip to content

Add support for Agent Identities. (#3396) #3402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ jobs:
run: msbuild Microsoft.Identity.Web.sln -r -t:build -verbosity:m -property:Configuration=Release

- name: Test with .NET 8.0.x
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net8.0 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)"
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net8.0 -v m -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)"

- name: Test with .NET 9.0.x
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net9.0 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)"
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net9.0 -v m -p:FROM_GITHUB_ACTION=true --configuration Release --collect "Xplat Code Coverage" --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)"

- name: Test with .NET 6.0.x
run: dotnet test Microsoft.Identity.Web.sln -f net6.0 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)"
run: dotnet test Microsoft.Identity.Web.sln -f net6.0 -v m -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~TokenAcquirerTests)&(FullyQualifiedName!~AgentApplicationsTests)"

- name: Create code coverage report
run: |
Expand Down Expand Up @@ -77,10 +77,7 @@ jobs:
# })

- name: Test with .NET 462
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net462 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)"
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net462 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~AgentApplicationsTests)"

- name: Test with .NET 472
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net472 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)"

- name: Test with .NET Standard 2.0
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f netstandard2.0 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)"
run: dotnet test --no-restore --no-build Microsoft.Identity.Web.sln -f net472 -v normal -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)&(FullyQualifiedName!~AgentApplicationsTests)"
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<!--This should be passed from the VSTS build-->
<!-- This needs to be greater than or equal to the validation baseline version -->
<MicrosoftIdentityWebVersion Condition="'$(MicrosoftIdentityWebVersion)' == ''">3.9.4</MicrosoftIdentityWebVersion>
<MicrosoftIdentityWebVersion Condition="'$(MicrosoftIdentityWebVersion)' == ''">3.10.0</MicrosoftIdentityWebVersion>
<!--This will generate AssemblyVersion, AssemblyFileVersion and AssemblyInformationVersion-->
<Version>$(MicrosoftIdentityWebVersion)</Version>

Expand Down
14 changes: 14 additions & 0 deletions Microsoft.Identity.Web.sln
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Oidc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.UI.Test", "tests\Microsoft.Identity.Web.UI.Test\Microsoft.Identity.Web.UI.Test.csproj", "{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentApplicationsTests", "tests\E2E Tests\AgentApplications\AgentApplicationsTests.csproj", "{DD56CDF7-E6B3-4304-B8DF-3AC610C35623}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.AgentIdentities", "src\Microsoft.Identity.Web.AgentIdentities\Microsoft.Identity.Web.AgentIdentities.csproj", "{C14780ED-5756-2A09-C6A7-5DDA433D1E86}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "daemon-app-msi", "tests\DevApps\daemon-app\daemon-app-msi\daemon-app-msi.csproj", "{A8181404-23E0-D38B-454C-D16ECDB18B9F}"
EndProject
Global
Expand Down Expand Up @@ -389,6 +393,14 @@ Global
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.Build.0 = Release|Any CPU
{DD56CDF7-E6B3-4304-B8DF-3AC610C35623}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD56CDF7-E6B3-4304-B8DF-3AC610C35623}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD56CDF7-E6B3-4304-B8DF-3AC610C35623}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD56CDF7-E6B3-4304-B8DF-3AC610C35623}.Release|Any CPU.Build.0 = Release|Any CPU
{C14780ED-5756-2A09-C6A7-5DDA433D1E86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C14780ED-5756-2A09-C6A7-5DDA433D1E86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C14780ED-5756-2A09-C6A7-5DDA433D1E86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C14780ED-5756-2A09-C6A7-5DDA433D1E86}.Release|Any CPU.Build.0 = Release|Any CPU
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -467,6 +479,8 @@ Global
{E927D215-A96C-626C-9A1A-CF99876FE7B4} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8} = {B4E72F1C-603F-437C-AAA1-153A604CD34A}
{DD56CDF7-E6B3-4304-B8DF-3AC610C35623} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
{C14780ED-5756-2A09-C6A7-5DDA433D1E86} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
{A8181404-23E0-D38B-454C-D16ECDB18B9F} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extensions methods to enable agent identities in Microsoft.Identity.Web.
/// </summary>
public static class AgentIdentityExtension
{
/// <summary>
/// Enable support for agent identities.
/// </summary>
/// <param name="services">Service collection</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddAgentIdentities(this IServiceCollection services)
{
Throws.IfNull(services);

// Register the OidcFic services for agent applications to work.
services.AddOidcFic();

return services;
}


/// <summary>
/// Updates the options to acquire a token for the agent identity.
/// </summary>
/// <param name="options">Authorization header provider options.</param>
/// <param name="agentApplicationId">The agent identity GUID.</param>
/// <returns>The updated authorization header provider options.</returns>
public static AuthorizationHeaderProviderOptions WithAgentIdentity(this AuthorizationHeaderProviderOptions options, string agentApplicationId)
{
// It's possible to start with no options, so we initialize it if it's null.
if (options == null)
options = new AuthorizationHeaderProviderOptions();

// AcquireTokenOptions holds the information needed to acquire a token for the Agent Identity
options.AcquireTokenOptions ??= new AcquireTokenOptions();
options.AcquireTokenOptions.ForAgentIdentity(agentApplicationId);

return options;
}

// TODO:make public?
private static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions options, string agentApplicationId)
{
options.ExtraParameters ??= new Dictionary<string, object>();

// Until it makes it way through Abstractions
options.ExtraParameters["fmiPathForClientAssertion"] = agentApplicationId;

// TODO: do we want to expose a mechanism to override the MicrosoftIdentityOptions instead of leveraging
// the default configuration section / named options?.
options.ExtraParameters["MicrosoftIdentityOptions"] = new MicrosoftEntraApplicationOptions
{
ClientId = agentApplicationId, // Agent identity Client ID.
ClientCredentials = [ new CredentialDescription() {
SourceType = CredentialSource.CustomSignedAssertion,
CustomSignedAssertionProviderName = "OidcIdpSignedAssertion",
CustomSignedAssertionProviderData = new Dictionary<string, object> {
{ "ConfigurationSection", "AzureAd" }, // Use the default configuration section name
{ "RequiresSignedAssertionFmiPath", true }, // The OidcIdpSignedAssertionProvider will require the fmiPath to be provided in the assertionRequestOptions.
}
}]
};
return options;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>

<Title>Microsoft Identity Web Agentic Identity support</Title>
<Product>Microsoft Identity Web for Agent Identities</Product>
<Description>Helper methods for Agent applications to act as the agent identities.</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>

<!-- The package is new in 3.10.0.-->
<PackageValidationBaselineVersion>3.10.0</PackageValidationBaselineVersion>
<EnablePackageValidation>false</EnablePackageValidation>

</PropertyGroup>

<ItemGroup>
<None Include="..\..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="PublicAPI\PublicAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI\PublicAPI.Unshipped.txt" />
<AdditionalFiles Include="PublicAPI\InternalAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI\InternalAPI.Unshipped.txt" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Identity.Web.Diagnostics\Microsoft.Identity.Web.Diagnostics.csproj" />
<ProjectReference Include="..\Microsoft.Identity.Web.OidcFIC\Microsoft.Identity.Web.OidcFIC.csproj" />
<ProjectReference Include="..\Microsoft.Identity.Web.TokenAcquisition\Microsoft.Identity.Web.TokenAcquisition.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#nullable enable
Microsoft.Identity.Web.AgentIdentityExtension
static Microsoft.Identity.Web.AgentIdentityExtension.AddAgentIdentities(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Identity.Web.AgentIdentityExtension.WithAgentIdentity(this Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions! options, string! agentApplicationId) -> Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions!
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

// Allow this assembly to be serviced when run on desktop CLR
[assembly: InternalsVisibleTo("Microsoft.Identity.Web, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.AgentIdentities, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.Certificate, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.CertificateLess, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.DownstreamApi, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Compile Update="DownstreamApi.HttpMethods.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>DownstreamApi.HttpMethods.tt</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Shipped.txt" />
Expand All @@ -46,4 +39,12 @@
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/InternalAPI.Unshipped.txt" />
</ItemGroup>

<ItemGroup>
<Compile Update="DownstreamApi.HttpMethods.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>DownstreamApi.HttpMethods.tt</DependentUpon>
</Compile>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web.OidcFic;

Expand All @@ -19,7 +20,7 @@ public static class OidcFicSignedAssertionProviderExtensions
/// <returns>the service collection for chaining.</returns>
public static IServiceCollection AddOidcFic(this IServiceCollection services)
{
services.AddSingleton<ICustomSignedAssertionProvider, OidcIdpSignedAssertionLoader>();
services.TryAddSingleton<ICustomSignedAssertionProvider, OidcIdpSignedAssertionLoader>();
return services;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ public async Task LoadIfNeededAsync(CredentialDescription credentialDescription,
_configuration.GetSection(sectionName).Bind(microsoftIdentityApplicationOptions);
}

signedAssertion = new OidcIdpSignedAssertionProvider(_tokenAcquirerFactory, microsoftIdentityApplicationOptions, credentialDescription.TokenExchangeUrl);
// Special case for Signed assertions with an FmiPath.
// The provider needs to postpone getting the signed assertion until the first call, when ClientAssertionFmiPath will be provided.
signedAssertion = new OidcIdpSignedAssertionProvider(_tokenAcquirerFactory, microsoftIdentityApplicationOptions, credentialDescription.TokenExchangeUrl, _logger);
if (credentialDescription.CustomSignedAssertionProviderData.TryGetValue("RequiresSignedAssertionFmiPath", out object? requiresSignedAssertionFmiPathObj) && requiresSignedAssertionFmiPathObj is bool requiresSignedAssertionFmiPathBool && requiresSignedAssertionFmiPathBool)
{
signedAssertion.RequiresSignedAssertionFmiPath = true;
}
}

try
Expand Down
Loading
Loading