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 @@ -87,7 +87,7 @@
<MicrosoftGraphVersion>4.36.0</MicrosoftGraphVersion>
<MicrosoftGraphBetaVersion>4.57.0-preview</MicrosoftGraphBetaVersion>
<MicrosoftExtensionsHttpVersion>3.1.3</MicrosoftExtensionsHttpVersion>
<MicrosoftIdentityAbstractions>5.3.0</MicrosoftIdentityAbstractions>
<MicrosoftIdentityAbstractions>6.0.0</MicrosoftIdentityAbstractions>
<NetNineRuntimeVersion> 9.0.0-preview.4.24266.19</NetNineRuntimeVersion>
<AspNetCoreNineRuntimeVersion> 9.0.0-preview.4.24267.6</AspNetCoreNineRuntimeVersion>
</PropertyGroup>
Expand Down
4 changes: 1 addition & 3 deletions benchmark/TokenAcquisitionBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using BenchmarkDotNet.Diagnostics.Windows;
using BenchmarkDotNet.Diagnostics.Windows.Configs;

namespace Benchmarks
{
Expand Down Expand Up @@ -40,7 +38,7 @@ public async Task CreateAuthorizationHeader()
{
// Get the authorization request creator service
IAuthorizationHeaderProvider authorizationHeaderProvider = s_serviceProvider!.GetRequiredService<IAuthorizationHeaderProvider>();
await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default");
await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default").ConfigureAwait(false);
}

[Benchmark]
Expand Down
38 changes: 18 additions & 20 deletions src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,11 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
HttpContent? httpContent;

if (effectiveOptions.Serializer != null)
{
{
httpContent = effectiveOptions.Serializer(input);
}
else
{
}
else
{
// if the input is already an HttpContent, it's used as is, and should already contain a ContentType.
httpContent = input switch
{
Expand Down Expand Up @@ -304,15 +304,15 @@ internal async Task<HttpResponseMessage> CallApiInternalAsync(
CancellationToken cancellationToken = default)
{
// Downstream API URI
string apiUrl = effectiveOptions.GetApiUrl();
string apiUrl = effectiveOptions.GetApiUrl();

// Create an HTTP request message
using HttpRequestMessage httpRequestMessage = new(
new HttpMethod(effectiveOptions.HttpMethod),
apiUrl);

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

using HttpClient client = string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName);

// Send the HTTP message
Expand Down Expand Up @@ -351,26 +351,24 @@ internal async Task UpdateRequestAsync(
httpRequestMessage.Content = content;
}

effectiveOptions.RequestAppToken = appToken;

// 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 = appToken ?
await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(
effectiveOptions.Scopes.FirstOrDefault()!,
effectiveOptions,
cancellationToken).ConfigureAwait(false) :
await _authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
effectiveOptions.Scopes,
effectiveOptions,
user,
cancellationToken).ConfigureAwait(false);
string authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
effectiveOptions.Scopes,
effectiveOptions,
user,
cancellationToken).ConfigureAwait(false);

httpRequestMessage.Headers.Add(Authorization, authorizationHeader);
}
}
else
{
Logger.UnauthenticatedApiCall(_logger, null);
}
}
if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader))
{
httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ internal class GraphAuthenticationProvider : IAuthenticationProvider
readonly IAuthorizationHeaderProvider _authorizationHeaderProvider;
readonly GraphServiceClientOptions _defaultAuthenticationOptions;
private readonly string[] _graphUris = ["graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", "graph.microsoft.de", "microsoftgraph.chinacloudapi.cn", "canary.graph.microsoft.com", "graph.microsoft-ppe.com"];
readonly IEnumerable<string> _defaultGraphScope = ["https://graph.microsoft.com/.default"];

/// <summary>
/// Constructor from the authorization header provider.
/// Constructor for the authorization header provider.
/// </summary>
/// <param name="authorizationHeaderProvider"></param>
/// <param name="defaultAuthenticationOptions"></param>
Expand Down Expand Up @@ -86,21 +87,12 @@ public async Task AuthenticateRequestAsync(
// Add the authorization header
if (allowedHostsValidator.IsUrlHostValid(request.URI) && !request.Headers.ContainsKey(AuthorizationHeaderKey))
{
string authorizationHeader;
if (authorizationHeaderProviderOptions!.RequestAppToken)
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default",
string authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
authorizationHeaderProviderOptions!.RequestAppToken ? _defaultGraphScope : scopes!,
authorizationHeaderProviderOptions,
cancellationToken);
}
else
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
scopes!,
authorizationHeaderProviderOptions,
claimsPrincipal: user,
cancellationToken);
}
user,
cancellationToken).ConfigureAwait(false);

request.Headers.Add(AuthorizationHeaderKey, authorizationHeader);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
Expand All @@ -16,13 +17,14 @@ namespace Microsoft.Identity.Web
internal class TokenAcquisitionAuthenticationProvider : IAuthenticationProvider
{
public TokenAcquisitionAuthenticationProvider(IAuthorizationHeaderProvider authorizationHeaderProvider, TokenAcquisitionAuthenticationProviderOption options)
{
{
_authorizationHeaderProvider = authorizationHeaderProvider;
_initialOptions = options;
}

private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider;
private readonly TokenAcquisitionAuthenticationProviderOption _initialOptions;
private readonly IEnumerable<string> _defaultGraphScope = ["https://graph.microsoft.com/.default"];

/// <summary>
/// Adds an authorization header to an HttpRequestMessage.
Expand Down Expand Up @@ -55,26 +57,17 @@ public async Task AuthenticateRequestAsync(HttpRequestMessage request)
DownstreamApiOptions? downstreamOptions = new DownstreamApiOptions() { BaseUrl = "https://graph.microsoft.com", Scopes = scopes };
downstreamOptions.AcquireTokenOptions.AuthenticationOptionsName = scheme;
downstreamOptions.AcquireTokenOptions.Tenant = tenant;
downstreamOptions.RequestAppToken = appOnly;

if (msalAuthProviderOption?.AuthorizationHeaderProviderOptions != null)
{
msalAuthProviderOption.AuthorizationHeaderProviderOptions(downstreamOptions);
}

string authorizationHeader;
if (appOnly)
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(
Constants.DefaultGraphScope,
downstreamOptions).ConfigureAwait(false);
}
else
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
scopes!,
string authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
appOnly ? _defaultGraphScope : scopes!,
downstreamOptions,
claimsPrincipal: user).ConfigureAwait(false);
}
user).ConfigureAwait(false);

// add or replace authorization header
if (request.Headers.Contains(Constants.Authorization))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -29,21 +27,35 @@ public BaseAuthorizationHeaderProvider(IServiceProvider serviceProvider)
// in the public API as it's going to be deprecated in future versions of IdWeb. Here this
// is an implementation detail.
var _tokenAcquisition = serviceProvider.GetRequiredService<ITokenAcquisition>();
implementation = new DefaultAuthorizationHeaderProvider(_tokenAcquisition);
_headerProvider = new DefaultAuthorizationHeaderProvider(_tokenAcquisition);
}

private IAuthorizationHeaderProvider implementation;
private readonly IAuthorizationHeaderProvider _headerProvider;

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

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

/// <inheritdoc/>
public virtual Task<string> CreateAuthorizationHeaderAsync(
IEnumerable<string> scopes,
AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null,
ClaimsPrincipal? claimsPrincipal = null,
CancellationToken cancellationToken = default)
{
return _headerProvider.CreateAuthorizationHeaderAsync(
scopes,
authorizationHeaderProviderOptions,
claimsPrincipal,
cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -50,6 +51,46 @@ public async Task<string> CreateAuthorizationHeaderForAppAsync(
return result.CreateAuthorizationHeader();
}

/// <inheritdoc/>
public async Task<string> CreateAuthorizationHeaderAsync(
IEnumerable<string> scopes,
AuthorizationHeaderProviderOptions? downstreamApiOptions = null,
ClaimsPrincipal? claimsPrincipal = null,
CancellationToken cancellationToken = default)
{
Client.AuthenticationResult result;

// Previously, with the API name we were able to distinguish between app and user token acquisition
// This context is missing in the new API, so can we enforce that downstreamApiOptions.RequestAppToken
// needs to be set to true to acquire a token for the app. We cannot rely on ClaimsPrincipal as it can be null for user token acquisition.
// DevEx Before:
// await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default").ConfigureAwait(false);
// DevEx with the new API:
// await authorizationHeaderProvider.CreateAuthorizationHeaderAsync(
// new [] { "https://graph.microsoft.com/.default" },
// new AuthorizationHeaderProviderOptions { RequestAppToken = true }).ConfigureAwait(false);
if (downstreamApiOptions != null && downstreamApiOptions.RequestAppToken)
{
result = await _tokenAcquisition.GetAuthenticationResultForAppAsync(
scopes.FirstOrDefault()!,
downstreamApiOptions?.AcquireTokenOptions.AuthenticationOptionsName,
downstreamApiOptions?.AcquireTokenOptions.Tenant,
CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken)).ConfigureAwait(false);
return result.CreateAuthorizationHeader();
}
else
{
result = await _tokenAcquisition.GetAuthenticationResultForUserAsync(
scopes,
downstreamApiOptions?.AcquireTokenOptions.AuthenticationOptionsName,
downstreamApiOptions?.AcquireTokenOptions.Tenant,
downstreamApiOptions?.AcquireTokenOptions.UserFlow,
claimsPrincipal,
CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken)).ConfigureAwait(false);
return result.CreateAuthorizationHeader();
}
}

private static TokenAcquisitionOptions CreateTokenAcquisitionOptionsFromApiOptions(
AuthorizationHeaderProviderOptions? downstreamApiOptions,
CancellationToken cancellationToken)
Expand Down
5 changes: 3 additions & 2 deletions tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Graph;
Expand Down Expand Up @@ -427,6 +425,9 @@ public async Task AcquireTokenWithManagedIdentity_UserAssigned()

// Assert: Make sure we got a token
Assert.False(string.IsNullOrEmpty(result));

result = await api.CreateAuthorizationHeaderAsync([scope], GetAuthHeaderOptions_ManagedId(baseUrl, clientId));
Assert.False(string.IsNullOrEmpty(result));
}

private static AuthorizationHeaderProviderOptions GetAuthHeaderOptions_ManagedId(string baseUrl, string? userAssignedClientId = null)
Expand Down
Loading