Skip to content

Commit d431c6d

Browse files
authored
Support for Agent User identities (#3435)
Added support for Agent User Identity.
1 parent c13bed5 commit d431c6d

File tree

39 files changed

+765
-116
lines changed

39 files changed

+765
-116
lines changed

.github/workflows/dotnetcore.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ jobs:
4343
run: msbuild Microsoft.Identity.Web.sln -r -t:build -verbosity:m -property:Configuration=Release
4444

4545
- name: Test with .NET 8.0.x
46-
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)"
46+
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)&(FullyQualifiedName!~AgentApplicationsTests)"
4747

4848
- name: Test with .NET 9.0.x
49-
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)"
49+
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)&(FullyQualifiedName!~AgentApplicationsTests)"
5050

5151
- name: Test with .NET 6.0.x
5252
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)"

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<!--This should be passed from the VSTS build-->
44
<!-- This needs to be greater than or equal to the validation baseline version -->
5-
<MicrosoftIdentityWebVersion Condition="'$(MicrosoftIdentityWebVersion)' == ''">3.10.1</MicrosoftIdentityWebVersion>
5+
<MicrosoftIdentityWebVersion Condition="'$(MicrosoftIdentityWebVersion)' == ''">3.12.0</MicrosoftIdentityWebVersion>
66
<!--This will generate AssemblyVersion, AssemblyFileVersion and AssemblyInformationVersion-->
77
<Version>$(MicrosoftIdentityWebVersion)</Version>
88

@@ -79,7 +79,7 @@
7979

8080
<PropertyGroup Label="Common dependency versions">
8181
<MicrosoftIdentityModelVersion Condition="'$(MicrosoftIdentityModelVersion)' == ''">8.12.1</MicrosoftIdentityModelVersion>
82-
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.73.1</MicrosoftIdentityClientVersion>
82+
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.74.1</MicrosoftIdentityClientVersion>
8383
<FxCopAnalyzersVersion>3.3.0</FxCopAnalyzersVersion>
8484
<SystemTextEncodingsWebVersion>4.7.2</SystemTextEncodingsWebVersion>
8585
<AzureSecurityKeyVaultSecretsVersion>4.6.0</AzureSecurityKeyVaultSecretsVersion>

src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using System;
54
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
95
using Microsoft.Extensions.DependencyInjection;
106
using Microsoft.Identity.Abstractions;
7+
using Microsoft.Identity.Web.AgentIdentities;
118

129
namespace Microsoft.Identity.Web
1310
{
@@ -28,10 +25,15 @@ public static IServiceCollection AddAgentIdentities(this IServiceCollection serv
2825
// Register the OidcFic services for agent applications to work.
2926
services.AddOidcFic();
3027

28+
// Register a callback to process the agent user identity before acquiring a token.
29+
services.Configure<TokenAcquisitionExtensionOptions>(options =>
30+
{
31+
options.OnBeforeTokenAcquisitionForTestUserAsync += AgentUserIdentityMsalAddIn.OnBeforeUserFicForAgentUserIdentityAsync;
32+
});
33+
3134
return services;
3235
}
3336

34-
3537
/// <summary>
3638
/// Updates the options to acquire a token for the agent identity.
3739
/// </summary>
@@ -51,24 +53,50 @@ public static AuthorizationHeaderProviderOptions WithAgentIdentity(this Authoriz
5153
return options;
5254
}
5355

54-
// TODO:make public?
55-
private static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions options, string agentApplicationId)
56+
/// <summary>
57+
/// Updates the options to acquire a token for the agent user identity.
58+
/// </summary>
59+
/// <param name="options">Authorization header provider options.</param>
60+
/// <param name="agentApplicationId">The agent identity GUID.</param>
61+
/// <param name="username">UPN of the user.</param>
62+
/// <returns>The updated authorization header provider options (in place. not a clone of the options).</returns>
63+
public static AuthorizationHeaderProviderOptions WithAgentUserIdentity(this AuthorizationHeaderProviderOptions options, string agentApplicationId, string username)
64+
{
65+
options ??= new AuthorizationHeaderProviderOptions();
66+
options.AcquireTokenOptions ??= new AcquireTokenOptions();
67+
options.AcquireTokenOptions.ExtraParameters ??= new Dictionary<string, object>();
68+
69+
// Set the agent application options
70+
options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] = new MicrosoftEntraApplicationOptions
71+
{
72+
ClientId = agentApplicationId, // Agent identity Client ID.
73+
};
74+
75+
// Set the username and agent identity parameters
76+
options.AcquireTokenOptions.ExtraParameters[Constants.UsernameKey] = username;
77+
options.AcquireTokenOptions.ExtraParameters[Constants.AgentIdentityKey] = agentApplicationId;
78+
79+
return options;
80+
}
81+
82+
// TODO:would it make sense to have it public?
83+
internal static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions options, string agentApplicationId)
5684
{
5785
options.ExtraParameters ??= new Dictionary<string, object>();
5886

5987
// Until it makes it way through Abstractions
60-
options.ExtraParameters["fmiPathForClientAssertion"] = agentApplicationId;
88+
options.ExtraParameters[Constants.FmiPathForClientAssertion] = agentApplicationId;
6189

6290
// TODO: do we want to expose a mechanism to override the MicrosoftIdentityOptions instead of leveraging
6391
// the default configuration section / named options?.
64-
options.ExtraParameters["MicrosoftIdentityOptions"] = new MicrosoftEntraApplicationOptions
92+
options.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] = new MicrosoftEntraApplicationOptions
6593
{
6694
ClientId = agentApplicationId, // Agent identity Client ID.
6795
ClientCredentials = [ new CredentialDescription() {
6896
SourceType = CredentialSource.CustomSignedAssertion,
6997
CustomSignedAssertionProviderName = "OidcIdpSignedAssertion",
7098
CustomSignedAssertionProviderData = new Dictionary<string, object> {
71-
{ "ConfigurationSection", "AzureAd" }, // Use the default configuration section name
99+
{ "ConfigurationSection", "AzureAd" }, // Use the default configuration section name
72100
{ "RequiresSignedAssertionFmiPath", true }, // The OidcIdpSignedAssertionProvider will require the fmiPath to be provided in the assertionRequestOptions.
73101
}
74102
}]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Claims;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Options;
9+
using Microsoft.Identity.Abstractions;
10+
using Microsoft.Identity.Client;
11+
using Microsoft.Identity.Client.Extensibility;
12+
13+
namespace Microsoft.Identity.Web.AgentIdentities
14+
{
15+
internal static class AgentUserIdentityMsalAddIn
16+
{
17+
internal static Task OnBeforeUserFicForAgentUserIdentityAsync(
18+
AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder builder,
19+
AcquireTokenOptions? options,
20+
ClaimsPrincipal user)
21+
{
22+
if (options == null || options.ExtraParameters == null)
23+
{
24+
return Task.CompletedTask;
25+
}
26+
IServiceProvider serviceProvider = (IServiceProvider)options.ExtraParameters![Constants.ExtensionOptionsServiceProviderKey];
27+
options.ExtraParameters.TryGetValue(Constants.AgentIdentityKey, out object? agentIdentityObject);
28+
options.ExtraParameters.TryGetValue(Constants.UsernameKey, out object? usernameObject);
29+
if (agentIdentityObject is string agentIdentity && usernameObject is string username)
30+
{
31+
// Register the MSAL extension that will modify the token request just in time.
32+
MsalAuthenticationExtension extension = new()
33+
{
34+
OnBeforeTokenRequestHandler = async (request) =>
35+
{
36+
// Get the services from the service provider.
37+
ITokenAcquirerFactory tokenAcquirerFactory = serviceProvider.GetRequiredService<ITokenAcquirerFactory>();
38+
IAuthenticationSchemeInformationProvider authenticationSchemeInformationProvider =
39+
serviceProvider.GetRequiredService<IAuthenticationSchemeInformationProvider>();
40+
IOptionsMonitor<MicrosoftIdentityApplicationOptions> optionsMonitor =
41+
serviceProvider.GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>();
42+
43+
// Get the FIC token for the agent application.
44+
string authenticationScheme = authenticationSchemeInformationProvider.GetEffectiveAuthenticationScheme(options.AuthenticationOptionsName);
45+
ITokenAcquirer tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer(authenticationScheme);
46+
ITokenAcquirer agentApplicationTokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer();
47+
AcquireTokenResult aaFic = await agentApplicationTokenAcquirer.GetFicTokenAsync(new() { FmiPath = agentIdentity }); // Uses the regular client credentials
48+
string? clientAssertion = aaFic.AccessToken;
49+
50+
// Get the FIC token for the agent identity.
51+
MicrosoftIdentityApplicationOptions microsoftIdentityApplicationOptions = optionsMonitor.Get(authenticationScheme);
52+
ITokenAcquirer agentIdentityTokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer(new MicrosoftIdentityApplicationOptions
53+
{
54+
ClientId = agentIdentity,
55+
Instance = microsoftIdentityApplicationOptions.Instance,
56+
Authority = microsoftIdentityApplicationOptions.Authority,
57+
TenantId = microsoftIdentityApplicationOptions.TenantId
58+
});
59+
AcquireTokenResult aidFic = await agentIdentityTokenAcquirer.GetFicTokenAsync(clientAssertion: clientAssertion); // Uses the agent identity
60+
string? userFicAssertion = aidFic.AccessToken;
61+
62+
// Already in the request:
63+
// - client_id = agentIdentity;
64+
65+
// User FIC parameters
66+
request.BodyParameters["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
67+
request.BodyParameters["client_assertion"] = clientAssertion;
68+
request.BodyParameters["username"] = username;
69+
request.BodyParameters["user_federated_identity_credential"] = userFicAssertion;
70+
request.BodyParameters["grant_type"] = "user_fic";
71+
request.BodyParameters.Remove("password");
72+
73+
if (request.BodyParameters.TryGetValue("client_secret", out var secret)
74+
&& secret.Equals("default", StringComparison.OrdinalIgnoreCase))
75+
{
76+
request.BodyParameters.Remove("client_secret");
77+
}
78+
}
79+
};
80+
builder.WithAuthenticationExtension(extension);
81+
}
82+
return Task.CompletedTask;
83+
}
84+
}
85+
}

src/Microsoft.Identity.Web.AgentIdentities/Microsoft.Identity.Web.AgentIdentities.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
<!-- The package is new in 3.10.0.-->
1010
<PackageValidationBaselineVersion>3.10.0</PackageValidationBaselineVersion>
11-
<EnablePackageValidation>false</EnablePackageValidation>
12-
11+
<EnablePackageValidation>true</EnablePackageValidation>
1312
</PropertyGroup>
1413

1514
<ItemGroup>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
#nullable enable
2+
Microsoft.Identity.Web.AgentIdentities.AgentUserIdentityMsalAddIn
3+
Microsoft.Identity.Web.AgentIdentities.AgentUserIdentityMsalAddIn.AgentUserIdentityMsalAddIn() -> void
4+
static Microsoft.Identity.Web.AgentIdentities.AgentUserIdentityMsalAddIn.OnBeforeUserFicForAgentUserIdentityAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? options, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task!
5+
static Microsoft.Identity.Web.AgentIdentityExtension.ForAgentIdentity(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! agentApplicationId) -> Microsoft.Identity.Abstractions.AcquireTokenOptions!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
static Microsoft.Identity.Web.AgentIdentityExtension.WithAgentUserIdentity(this Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions! options, string! agentApplicationId, string! username) -> Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions!

src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionLoader.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public static void SignedAssertionProviderFailed(
153153
}
154154

155155

156+
156157
public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters = null)
157158
{
158159
OidcIdpSignedAssertionProvider? signedAssertion = credentialDescription.CachedValue as OidcIdpSignedAssertionProvider;

src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ public static class Constants
134134
internal const string CiamAuthoritySuffix = ".ciamlogin.com";
135135
internal const string TestSlice = "dc";
136136
internal const string ExtensionOptionsServiceProviderKey = "ID_WEB_INTERNAL_SERVICE_PROVIDER";
137+
internal const string AgentIdentityKey = "IDWEB_AGENT_IDENTITY";
138+
internal const string UsernameKey = "IDWEB_USERNAME";
139+
internal const string FmiPathForClientAssertion = "IDWEB_FMI_PATH_FOR_CLIENT_ASSERTION";
140+
internal const string MicrosoftIdentityOptionsParameter = "IDWEB_FMI_MICROSOFT_IDENTITY_OPTIONS";
141+
142+
/// <summary>
143+
/// Key for client assertion in token acquisition options.
144+
/// </summary>
145+
internal const string ClientAssertion = "IDWEB_CLIENT_ASSERTION";
137146

138147
// Blazor challenge URI
139148
internal const string BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=";

0 commit comments

Comments
 (0)