Skip to content
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

<PropertyGroup Label="Common dependency versions">
<MicrosoftIdentityModelVersion Condition="'$(MicrosoftIdentityModelVersion)' == ''">8.18.0</MicrosoftIdentityModelVersion>
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.84.0</MicrosoftIdentityClientVersion>
<MicrosoftIdentityClientVersion Condition="'$(MicrosoftIdentityClientVersion)' == ''">4.84.1</MicrosoftIdentityClientVersion>
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">12.0.0</MicrosoftIdentityAbstractionsVersion>
<FxCopAnalyzersVersion>3.3.0</FxCopAnalyzersVersion>
<SystemTextEncodingsWebVersion>4.7.2</SystemTextEncodingsWebVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;

namespace Microsoft.Identity.Web.Extensibility
{
/// <summary>
/// Extension methods for <see cref="TokenAcquisitionOptions"/> in extensibility scenarios.
/// </summary>
public static class TokenAcquisitionOptionsExtensions
{
/// <summary>
/// Sets cache partition key-value pairs on the options. When set, the token cache
/// lookup and storage will include these components, isolating cached tokens from
/// entries that have different (or no) partition keys.
/// </summary>
/// <param name="options">The token acquisition options.</param>
/// <param name="partitionKeys">The partition key-value pairs.</param>
/// <returns>The options instance for chaining.</returns>
public static TokenAcquisitionOptions WithCachePartitionKeys(
this TokenAcquisitionOptions options,
IDictionary<string, string> partitionKeys)
{
options.CachePartitionKeys = partitionKeys;
return options;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Threading.Tasks;
using Microsoft.Identity.Client;

namespace Microsoft.Identity.Web.Extensibility
{
/// <summary>
/// Provides access to the <see cref="IConfidentialClientApplication"/> managed by
/// Microsoft.Identity.Web for a given authentication scheme. Use this when you need
/// to call MSAL directly (e.g., for custom token acquisition flows) while reusing
/// the same application instance, credentials, and token cache that IdWeb manages.
/// </summary>
public interface IConfidentialClientApplicationProvider
{
/// <summary>
/// Gets the <see cref="IConfidentialClientApplication"/> for the specified authentication scheme.
/// </summary>
/// <param name="authenticationScheme">
/// The authentication scheme name. If null, the effective default scheme is used.
/// </param>
/// <returns>The confidential client application instance.</returns>
Task<IConfidentialClientApplication> GetConfidentialClientApplicationAsync(
string? authenticationScheme = null);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider
Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.IConfidentialClientApplication!>!
Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions
static Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions.WithCachePartitionKeys(this Microsoft.Identity.Web.TokenAcquisitionOptions! options, System.Collections.Generic.IDictionary<string!, string!>! partitionKeys) -> Microsoft.Identity.Web.TokenAcquisitionOptions!
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider
Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.IConfidentialClientApplication!>!
Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions
static Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions.WithCachePartitionKeys(this Microsoft.Identity.Web.TokenAcquisitionOptions! options, System.Collections.Generic.IDictionary<string!, string!>! partitionKeys) -> Microsoft.Identity.Web.TokenAcquisitionOptions!
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Extensibility;
using Microsoft.Identity.Web.Hosts;

namespace Microsoft.Identity.Web
Expand Down Expand Up @@ -71,6 +72,7 @@ public static IServiceCollection AddTokenAcquisition(

ServiceDescriptor? tokenAcquisitionService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisition));
ServiceDescriptor? tokenAcquisitionInternalService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisitionInternal));
ServiceDescriptor? ccaProviderService = services.FirstOrDefault(s => s.ServiceType == typeof(IConfidentialClientApplicationProvider));
ServiceDescriptor? tokenAcquisitionhost = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisitionHost));
ServiceDescriptor? authenticationHeaderCreator = services.FirstOrDefault(s => s.ServiceType == typeof(IAuthorizationHeaderProvider));
ServiceDescriptor? tokenAcquirerFactory = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquirerFactory));
Expand All @@ -89,6 +91,11 @@ public static IServiceCollection AddTokenAcquisition(
services.Remove(authenticationHeaderCreator);
services.Remove(authSchemeInfoProvider);

if (ccaProviderService != null)
{
services.Remove(ccaProviderService);
}

if (credentialsProviderService != null)
{
services.Remove(credentialsProviderService);
Expand Down Expand Up @@ -137,6 +144,7 @@ public static IServiceCollection AddTokenAcquisition(
}
#endif
services.AddSingleton(s => (ITokenAcquisitionInternal)s.GetRequiredService<ITokenAcquisition>());
services.AddSingleton(s => (IConfidentialClientApplicationProvider)s.GetRequiredService<ITokenAcquisition>());
Comment thread
iNinja marked this conversation as resolved.
services.AddSingleton<Abstractions.IAuthenticationSchemeInformationProvider>(sp =>
sp.GetRequiredService<ITokenAcquisitionHost>());
services.AddSingleton<IAuthorizationHeaderProvider, DefaultAuthorizationHeaderProvider>();
Expand Down Expand Up @@ -171,6 +179,7 @@ public static IServiceCollection AddTokenAcquisition(
}
#endif
services.AddScoped(s => (ITokenAcquisitionInternal)s.GetRequiredService<ITokenAcquisition>());
services.AddScoped(s => (IConfidentialClientApplicationProvider)s.GetRequiredService<ITokenAcquisition>());
services.AddScoped<Abstractions.IAuthenticationSchemeInformationProvider>(sp =>
sp.GetRequiredService<ITokenAcquisitionHost>());
services.AddScoped<IAuthorizationHeaderProvider, DefaultAuthorizationHeaderProvider>();
Expand Down
24 changes: 21 additions & 3 deletions src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Web.Experimental;
using Microsoft.Identity.Web.Extensibility;
using Microsoft.Identity.Web.TestOnly;
using Microsoft.Identity.Web.TokenCacheProviders;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
Expand All @@ -38,9 +39,9 @@ namespace Microsoft.Identity.Web
* Treat as a public member.
*/
#if NETSTANDARD2_0 || NET462 || NET472
internal partial class TokenAcquisition : ITokenAcquisitionInternal
internal partial class TokenAcquisition : ITokenAcquisitionInternal, IConfidentialClientApplicationProvider
#else
internal partial class TokenAcquisition
internal partial class TokenAcquisition : IConfidentialClientApplicationProvider
#endif
{
#if NETSTANDARD2_0 || NET462 || NET472
Expand Down Expand Up @@ -443,6 +444,8 @@ private async Task<AuthenticationResult> GetAuthenticationResultForUserInternalA
var account = await application.GetAccountAsync(user.GetMsalAccountId()).ConfigureAwait(false);

// Silent flow
// Note: CachePartitionKeys from TokenAcquisitionOptions is not applied here.
// This path is used by ROPC, which does not support TokenAcquisitionOptions.
return await application.AcquireTokenSilent(
scopes.Except(_scopesRequestedByMsal),
account)
Expand Down Expand Up @@ -1038,7 +1041,15 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti
return clientClaims;
}

#pragma warning disable RS0051 // Add internal types and members to the declared API
/// <inheritdoc/>
public async Task<IConfidentialClientApplication> GetConfidentialClientApplicationAsync(
string? authenticationScheme = null)
{
MergedOptions mergedOptions = _tokenAcquisitionHost.GetOptions(authenticationScheme, out _);
return await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false)
.ConfigureAwait(false);
}

internal /* for testing */ async Task<IConfidentialClientApplication> GetOrBuildConfidentialClientApplicationAsync(
MergedOptions mergedOptions,
bool isTokenBinding)
Expand Down Expand Up @@ -1562,6 +1573,13 @@ private Task<AuthenticationResult> GetAuthenticationResultForWebAppWithAccountFr
{
builder.WithProofOfPossession(tokenAcquisitionOptions.PoPConfiguration);
}
if (tokenAcquisitionOptions.CachePartitionKeys != null)
{
foreach (var kvp in tokenAcquisitionOptions.CachePartitionKeys)
{
builder.WithCachePartitionKey(kvp.Key, kvp.Value);
}
}
}

// Acquire an access token as a B2C authority
Expand Down
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 System.Collections.Generic;
using System.Threading;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client.AppConfig;
Expand All @@ -25,6 +26,8 @@ public class TokenAcquisitionOptions : AcquireTokenOptions
/// </summary>
public CancellationToken CancellationToken { get; set; } = CancellationToken.None;

internal IDictionary<string, string>? CachePartitionKeys { get; set; }

/// <summary>
/// Clone the options (to be able to override them).
/// </summary>
Expand All @@ -46,7 +49,10 @@ public class TokenAcquisitionOptions : AcquireTokenOptions
CancellationToken = CancellationToken,
LongRunningWebApiSessionKey = LongRunningWebApiSessionKey,
ManagedIdentity = ManagedIdentity,
FmiPath = FmiPath
FmiPath = FmiPath,
CachePartitionKeys = CachePartitionKeys != null
? new Dictionary<string, string>(CachePartitionKeys)
: null
Comment thread
iNinja marked this conversation as resolved.
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Extensibility;
using Microsoft.Identity.Web.Test.Common;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using Xunit;

namespace Microsoft.Identity.Web.Test
Expand All @@ -25,7 +27,9 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime()
var services = new ServiceCollection();

services.AddTokenAcquisition();
ServiceDescriptor[] orderedServices = services.OrderBy(s => s.ServiceType.FullName).ToArray();
ServiceDescriptor[] orderedServices = services
.OrderBy(s => s.ServiceType.FullName)
.ToArray();

Assert.Collection(
orderedServices,
Expand Down Expand Up @@ -92,6 +96,14 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime()
Assert.Null(actual.ImplementationFactory);
},
actual =>
{
Assert.Equal(ServiceLifetime.Scoped, actual.Lifetime);
Assert.Equal(typeof(IConfidentialClientApplicationProvider), actual.ServiceType);
Assert.Null(actual.ImplementationType);
Assert.Null(actual.ImplementationInstance);
Assert.NotNull(actual.ImplementationFactory);
},
actual =>
{
Assert.Equal(typeof(ICredentialsProvider), actual.ServiceType);
Assert.Equal(typeof(CredentialsProvider), actual.ImplementationType);
Expand Down Expand Up @@ -143,7 +155,7 @@ public void AddTokenAcquisition_Sdk_SupportsKeyedServices()
services.AddTokenAcquisition();

// Verify the number of services added by AddTokenAcquisition (ignoring the service we added here).
Assert.Equal(13, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests)));
Assert.Equal(14, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests)));
}
#endif

Expand All @@ -161,6 +173,43 @@ public void AddTokenAcquisition_AbleToOverrideICredentialsLoader()
Assert.Single(orderedServices, s => s.ServiceType == typeof(ICredentialsLoader));
}

[Fact]
public void AddTokenAcquisition_RegistersIConfidentialClientApplicationProvider()
{
// Arrange
var services = new ServiceCollection();

// Act
services.AddTokenAcquisition();

// Assert
ServiceDescriptor serviceDescriptor = Assert.Single(services, s => s.ServiceType == typeof(IConfidentialClientApplicationProvider));
Assert.Equal(ServiceLifetime.Scoped, serviceDescriptor.Lifetime);
Assert.Null(serviceDescriptor.ImplementationType);
Assert.Null(serviceDescriptor.ImplementationInstance);
Assert.NotNull(serviceDescriptor.ImplementationFactory);
}

[Fact]
public void AddTokenAcquisition_ResolvesIConfidentialClientApplicationProviderToTokenAcquisition()
{
// Arrange
var services = new ServiceCollection();
services.AddTokenAcquisition();
services.AddInMemoryTokenCaches();
services.AddHttpClient();
using ServiceProvider serviceProvider = services.BuildServiceProvider();
using IServiceScope serviceScope = serviceProvider.CreateScope();

// Act
var tokenAcquisition = serviceScope.ServiceProvider.GetRequiredService<ITokenAcquisition>();
var confidentialClientApplicationProvider = serviceScope.ServiceProvider.GetRequiredService<IConfidentialClientApplicationProvider>();

// Assert
Assert.Same(tokenAcquisition, confidentialClientApplicationProvider);
Assert.IsAssignableFrom<TokenAcquisition>(confidentialClientApplicationProvider);
}

[Fact]
public void AddHttpContextAccessor_ThrowsWithoutServices()
{
Expand All @@ -181,7 +230,9 @@ public void AddTokenAcquisitionCalledTwice_RegistersTokenAcquisitionOnlyAsSingle
services.AddTokenAcquisition(isTokenAcquisitionSingleton: true);

// Assert
var orderedServices = services.OrderBy(s => s.ServiceType.FullName).ToList();
var orderedServices = services
.OrderBy(s => s.ServiceType.FullName)
.ToList();

// Check that the first service is registered as singleton
Assert.Collection(
Expand Down Expand Up @@ -248,6 +299,14 @@ public void AddTokenAcquisitionCalledTwice_RegistersTokenAcquisitionOnlyAsSingle
Assert.Null(actual.ImplementationFactory);
},
actual =>
{
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
Assert.Equal(typeof(IConfidentialClientApplicationProvider), actual.ServiceType);
Assert.Null(actual.ImplementationType);
Assert.Null(actual.ImplementationInstance);
Assert.NotNull(actual.ImplementationFactory);
},
actual =>
{
Assert.Equal(typeof(ICredentialsProvider), actual.ServiceType);
Assert.Equal(typeof(CredentialsProvider), actual.ImplementationType);
Expand Down
34 changes: 34 additions & 0 deletions tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Extensibility;
using Microsoft.Identity.Web.Test.Common;
using Microsoft.Identity.Web.Test.Common.Mocks;
using Microsoft.Identity.Web.TestOnly;
Expand All @@ -24,6 +25,39 @@ public class TokenAcquisitionTests
private const string TenantId = "tenant-id";
private const string AppHomeTenantId = "app-home-tenant-id";

[Fact]
public void CachePartitionKeys_DefaultsToNull()
{
// Arrange
var options = new TokenAcquisitionOptions();

// Act
IDictionary<string, string>? cachePartitionKeys = options.CachePartitionKeys;

// Assert
Assert.Null(cachePartitionKeys);
}

[Fact]
public void CachePartitionKeys_CanBeSet()
{
// Arrange
IDictionary<string, string> cachePartitionKeys = new Dictionary<string, string>
{
["tenant"] = "contoso",
["user"] = "alice"
};

// Act
var options = new TokenAcquisitionOptions()
.WithCachePartitionKeys(cachePartitionKeys);

// Assert
Assert.Same(cachePartitionKeys, options.CachePartitionKeys);
Assert.Equal("contoso", options.CachePartitionKeys!["tenant"]);
Assert.Equal("alice", options.CachePartitionKeys["user"]);
}

[Theory]
[InlineData(null, null, null, null)]
[InlineData(null, null, AppHomeTenantId, null)]
Expand Down
Loading