Skip to content
Draft
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
4 changes: 4 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net472' or '$(TargetFramework)' == 'netstandard2.0'">
<DefineConstants>$(DefineConstants);SUPPORTS_MTLS;</DefineConstants>
</PropertyGroup>
</Project>
26 changes: 17 additions & 9 deletions src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
Expand All @@ -25,7 +26,7 @@ namespace Microsoft.Identity.Web
internal partial class DownstreamApi : IDownstreamApi
{
private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMsalHttpClientFactory _httpClientFactory;
private readonly IOptionsMonitor<DownstreamApiOptions> _namedDownstreamApiOptions;
private const string Authorization = "Authorization";
protected readonly ILogger<DownstreamApi> _logger;
Expand All @@ -36,17 +37,17 @@ internal partial class DownstreamApi : IDownstreamApi
/// </summary>
/// <param name="authorizationHeaderProvider">Authorization header provider.</param>
/// <param name="namedDownstreamApiOptions">Named options provider.</param>
/// <param name="httpClientFactory">HTTP client factory.</param>
/// <param name="msalHttpClientFactory">MSAL HTTP client factory.</param>
/// <param name="logger">Logger.</param>
public DownstreamApi(
IAuthorizationHeaderProvider authorizationHeaderProvider,
IOptionsMonitor<DownstreamApiOptions> namedDownstreamApiOptions,
IHttpClientFactory httpClientFactory,
IMsalHttpClientFactory msalHttpClientFactory,
ILogger<DownstreamApi> logger)
{
_authorizationHeaderProvider = authorizationHeaderProvider;
_namedDownstreamApiOptions = namedDownstreamApiOptions;
_httpClientFactory = httpClientFactory;
_httpClientFactory = msalHttpClientFactory;
_logger = logger;
}

Expand Down Expand Up @@ -514,11 +515,13 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
new HttpMethod(effectiveOptions.HttpMethod),
apiUrl);

await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken);
var authorizationHeaderInformation = await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken);

using HttpClient client = string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName);
using HttpClient client = _httpClientFactory is IMsalMtlsHttpClientFactory msalMtlsHttpClientFactory && authorizationHeaderInformation ?.BindingCertificate != null
? msalMtlsHttpClientFactory.GetHttpClient(authorizationHeaderInformation.BindingCertificate)
: _httpClientFactory.GetHttpClient();

// Send the HTTP message
// Send the HTTP message
var downstreamApiResult = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);

// Retry only if the resource sent 401 Unauthorized with WWW-Authenticate header and claims
Expand All @@ -541,7 +544,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
return downstreamApiResult;
}

internal /* internal for test */ async Task UpdateRequestAsync(
internal /* internal for test */ async Task<AuthorizationHeaderInformation?> UpdateRequestAsync(
HttpRequestMessage httpRequestMessage,
HttpContent? content,
DownstreamApiOptions effectiveOptions,
Expand All @@ -558,16 +561,19 @@ public Task<HttpResponseMessage> CallApiForAppAsync(

effectiveOptions.RequestAppToken = appToken;

AuthorizationHeaderInformation? authorizationHeaderInformation = null;

// Obtention of the authorization header (except when calling an anonymous endpoint
// which is done by not specifying any scopes
if (effectiveOptions.Scopes != null && effectiveOptions.Scopes.Any())
{
string authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
authorizationHeaderInformation = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
effectiveOptions.Scopes,
effectiveOptions,
user,
cancellationToken).ConfigureAwait(false);

var authorizationHeader = authorizationHeaderInformation.AuthorizationHeaderValue!;
if (authorizationHeader.StartsWith(AuthSchemeDstsSamlBearer, StringComparison.OrdinalIgnoreCase))
{
// TryAddWithoutValidation method bypasses strict validation, allowing non-standard headers to be added for custom Header schemes that cannot be parsed.
Expand Down Expand Up @@ -625,6 +631,8 @@ public Task<HttpResponseMessage> CallApiForAppAsync(

// Opportunity to change the request message
effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage);

return authorizationHeaderInformation;
}

internal /* for test */ static Dictionary<string, string> CallerSDKDetails { get; } = new()
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Identity.Abstractions.DownstreamApiOptions!>! namedDownstreamApiOptions, Microsoft.Identity.Client.IMsalHttpClientFactory! msalHttpClientFactory, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DownstreamApi!>! logger) -> void
Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Abstractions.AuthorizationHeaderInformation?>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Identity.Abstractions.DownstreamApiOptions!>! namedDownstreamApiOptions, Microsoft.Identity.Client.IMsalHttpClientFactory! msalHttpClientFactory, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DownstreamApi!>! logger) -> void
Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Abstractions.AuthorizationHeaderInformation?>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Identity.Abstractions.DownstreamApiOptions!>! namedDownstreamApiOptions, Microsoft.Identity.Client.IMsalHttpClientFactory! msalHttpClientFactory, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DownstreamApi!>! logger) -> void
Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Abstractions.AuthorizationHeaderInformation?>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Identity.Abstractions.DownstreamApiOptions!>! namedDownstreamApiOptions, Microsoft.Identity.Client.IMsalHttpClientFactory! msalHttpClientFactory, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DownstreamApi!>! logger) -> void
Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Abstractions.AuthorizationHeaderInformation?>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Identity.Abstractions.DownstreamApiOptions!>! namedDownstreamApiOptions, Microsoft.Identity.Client.IMsalHttpClientFactory! msalHttpClientFactory, Microsoft.Extensions.Logging.ILogger<Microsoft.Identity.Web.DownstreamApi!>! logger) -> void
Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Abstractions.AuthorizationHeaderInformation?>!
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ public BaseAuthorizationHeaderProvider(IServiceProvider serviceProvider)
private readonly IAuthorizationHeaderProvider _headerProvider;

/// <inheritdoc/>
public virtual Task<string> CreateAuthorizationHeaderForUserAsync(IEnumerable<string> scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default)
public virtual Task<AuthorizationHeaderInformation> CreateAuthorizationHeaderForUserAsync(IEnumerable<string> scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default)
{
return _headerProvider.CreateAuthorizationHeaderForUserAsync(scopes, authorizationHeaderProviderOptions, claimsPrincipal, cancellationToken);
}

/// <inheritdoc/>
public virtual Task<string> CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default)
public virtual Task<AuthorizationHeaderInformation> CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default)
{
return _headerProvider.CreateAuthorizationHeaderForAppAsync(scopes, downstreamApiOptions, cancellationToken);
}

/// <inheritdoc/>
public virtual Task<string> CreateAuthorizationHeaderAsync(
public virtual Task<AuthorizationHeaderInformation> CreateAuthorizationHeaderAsync(
IEnumerable<string> scopes,
AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null,
ClaimsPrincipal? claimsPrincipal = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public static async Task<ConfidentialClientApplicationBuilder> WithClientCredent
IEnumerable<CredentialDescription> clientCredentials,
ILogger logger,
ICredentialsLoader credentialsLoader,
CredentialSourceLoaderParameters? credentialSourceLoaderParameters)
CredentialSourceLoaderParameters? credentialSourceLoaderParameters,
bool isTokenBinding = false)
{
var credential = await LoadCredentialForMsalOrFailAsync(
clientCredentials,
Expand All @@ -44,6 +45,17 @@ public static async Task<ConfidentialClientApplicationBuilder> WithClientCredent
return builder;
}

// regardless of credential type being set, bound token requires a certificate
if (isTokenBinding)
{
if (credential.Certificate == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this works for a POC, but we probably need more abstraction here. Each credential may, or may not, be compatible with mTLS POP. In particular, I am thinking of the 2 FIC credentials.

CC @jmprieur @MZOLN

{
logger.LogError("Loaded credentials for token binding doesn't contain a certificate");
}

return builder.WithCertificate(credential.Certificate);
}

switch (credential.CredentialType)
{
case CredentialType.SignedAssertion:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public DefaultAuthorizationHeaderProvider(ITokenAcquisition tokenAcquisition)
}

/// <inheritdoc/>
public async Task<string> CreateAuthorizationHeaderForUserAsync(
public async Task<AuthorizationHeaderInformation> CreateAuthorizationHeaderForUserAsync(
IEnumerable<string> scopes,
AuthorizationHeaderProviderOptions? downstreamApiOptions = null,
ClaimsPrincipal? claimsPrincipal = null,
Expand All @@ -41,7 +41,7 @@ public async Task<string> CreateAuthorizationHeaderForUserAsync(
}

/// <inheritdoc/>
public async Task<string> CreateAuthorizationHeaderForAppAsync(
public async Task<AuthorizationHeaderInformation> CreateAuthorizationHeaderForAppAsync(
string scopes,
AuthorizationHeaderProviderOptions? downstreamApiOptions = null,
CancellationToken cancellationToken = default)
Expand All @@ -56,7 +56,7 @@ public async Task<string> CreateAuthorizationHeaderForAppAsync(
}

/// <inheritdoc/>
public async Task<string> CreateAuthorizationHeaderAsync(
public async Task<AuthorizationHeaderInformation> CreateAuthorizationHeaderAsync(
IEnumerable<string> scopes,
AuthorizationHeaderProviderOptions? downstreamApiOptions = null,
ClaimsPrincipal? claimsPrincipal = null,
Expand Down
Loading