diff --git a/src/Microsoft.Identity.Web/Constants/Constants.cs b/src/Microsoft.Identity.Web/Constants/Constants.cs
index 377475bb6..13fc02503 100644
--- a/src/Microsoft.Identity.Web/Constants/Constants.cs
+++ b/src/Microsoft.Identity.Web/Constants/Constants.cs
@@ -59,6 +59,7 @@ internal static class Constants
public const string LoginHint = "loginHint";
public const string DomainHint = "domainHint";
public const string Authorization = "Authorization";
+ public const string ApplicationJson = "application/json";
// Blazor challenge URI
public const string BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=";
diff --git a/src/Microsoft.Identity.Web/Constants/IDWebErrorMessage.cs b/src/Microsoft.Identity.Web/Constants/IDWebErrorMessage.cs
index 5c6eb766f..31d9805eb 100644
--- a/src/Microsoft.Identity.Web/Constants/IDWebErrorMessage.cs
+++ b/src/Microsoft.Identity.Web/Constants/IDWebErrorMessage.cs
@@ -11,6 +11,7 @@ internal static class IDWebErrorMessage
// General IDW10000 = "IDW10000:"
public const string HttpContextIsNull = "IDW10001: HttpContext is null. ";
public const string HttpContextAndHttpResponseAreNull = "IDW10002: Current HttpContext and HttpResponse argument are null. Pass an HttpResponse argument. ";
+ public const string InvalidHttpStatusCodeInResponse = "IDW10003: Invalid status code in the HttpResponseMessage. Status code: {0}, Error message: {1} .";
// Configuration IDW10100 = "IDW10100:"
public const string ProvideEitherScopeKeySectionOrScopes = "IDW10101: Either provide the '{0}' or the '{1}' to the 'AuthorizeForScopes'. ";
@@ -22,7 +23,8 @@ internal static class IDWebErrorMessage
"For instance, in the appsettings.json file. ";
public const string BothClientSecretAndCertificateProvided = "IDW10105: Both client secret and client certificate, " +
"cannot be included in the configuration of the web app when calling a web API. ";
- public const string ConfigurationOptionRequired = "The '{0}' option must be provided. ";
+ public const string ConfigurationOptionRequired = "IDW10106: The '{0}' option must be provided. ";
+ public const string ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. ";
// Authorization IDW10200 = "IDW10200:"
public const string NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope or roles claim was found in the bearer token. ";
diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs
new file mode 100644
index 000000000..d449913d3
--- /dev/null
+++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Identity.Web
+{
+ ///
+ /// Implementation for the downstream web API.
+ ///
+ public class DownstreamWebApi : IDownstreamWebApi
+ {
+ private readonly ITokenAcquisition _tokenAcquisition;
+ private readonly HttpClient _httpClient;
+ private readonly IOptionsMonitor _namedDownstreamWebApiOptions;
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ };
+
+ ///
+ /// Constructor.
+ ///
+ /// Token acquisition service.
+ /// Named options provider.
+ /// HTTP client.
+ public DownstreamWebApi(
+ ITokenAcquisition tokenAcquisition,
+ IOptionsMonitor namedDownstreamWebApiOptions,
+ HttpClient httpClient)
+ {
+ _tokenAcquisition = tokenAcquisition;
+ _namedDownstreamWebApiOptions = namedDownstreamWebApiOptions;
+ _httpClient = httpClient;
+ }
+
+ ///
+ public async Task CallWebApiForUserAsync(
+ string optionsInstanceName,
+ Action? calledDownstreamApiOptionsOverride,
+ ClaimsPrincipal? user,
+ StringContent? requestContent)
+ {
+ DownstreamWebApiOptions effectiveOptions = MergeOptions(optionsInstanceName, calledDownstreamApiOptionsOverride);
+
+ if (string.IsNullOrEmpty(effectiveOptions.Scopes))
+ {
+ throw new ArgumentException(IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate);
+ }
+
+ string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(
+ effectiveOptions.GetScopes(),
+ effectiveOptions.Tenant)
+ .ConfigureAwait(false);
+
+ HttpResponseMessage response;
+ using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(
+ effectiveOptions.HttpMethod,
+ effectiveOptions.GetApiUrl()))
+ {
+ if (requestContent != null)
+ {
+ httpRequestMessage.Content = requestContent;
+ }
+
+ httpRequestMessage.Headers.Add(
+ Constants.Authorization,
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} {1}",
+ Constants.Bearer,
+ accessToken));
+ response = await _httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
+ }
+
+ return response;
+ }
+
+ ///
+ /// Merge the options from configuration and override from caller.
+ ///
+ /// Named configuration.
+ /// Delegate to override the configuration.
+ internal /* for tests */ DownstreamWebApiOptions MergeOptions(
+ string optionsInstanceName,
+ Action? calledApiOptionsOverride)
+ {
+ // Gets the options from configuration (or default value)
+ DownstreamWebApiOptions options;
+ if (optionsInstanceName != null)
+ {
+ options = _namedDownstreamWebApiOptions.Get(optionsInstanceName);
+ }
+ else
+ {
+ options = _namedDownstreamWebApiOptions.CurrentValue;
+ }
+
+ DownstreamWebApiOptions clonedOptions = options.Clone();
+ calledApiOptionsOverride?.Invoke(clonedOptions);
+ return clonedOptions;
+ }
+
+ ///
+ public async Task CallWebApiForUserAsync(
+ string optionsInstanceName,
+ TInput input,
+ Action? downstreamWebApiOptionsOverride = null,
+ ClaimsPrincipal? user = null)
+ where TOutput : class
+ {
+ StringContent? jsoncontent;
+ if (input != null)
+ {
+ var jsonRequest = JsonSerializer.Serialize(input);
+ jsoncontent = new StringContent(jsonRequest, Encoding.UTF8, Constants.ApplicationJson);
+ }
+ else
+ {
+ jsoncontent = null;
+ }
+
+ HttpResponseMessage response = await CallWebApiForUserAsync(
+ optionsInstanceName,
+ downstreamWebApiOptionsOverride,
+ user,
+ jsoncontent).ConfigureAwait(false);
+
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ TOutput? output = JsonSerializer.Deserialize(content, _jsonOptions);
+ return output;
+ }
+ else
+ {
+ string error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new HttpRequestException(string.Format(
+ CultureInfo.InvariantCulture,
+ IDWebErrorMessage.InvalidHttpStatusCodeInResponse,
+ response.StatusCode,
+ error));
+ }
+ }
+
+ ///
+ public async Task CallWebApiForAppAsync(
+ string optionsInstanceName,
+ Action? downstreamApiOptionsOverride = null,
+ StringContent? requestContent = null)
+ {
+ DownstreamWebApiOptions effectiveOptions = MergeOptions(optionsInstanceName, downstreamApiOptionsOverride);
+
+ if (effectiveOptions.Scopes == null)
+ {
+ throw new ArgumentException(IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate);
+ }
+
+ string accessToken = await _tokenAcquisition.GetAccessTokenForAppAsync(
+ effectiveOptions.Scopes,
+ effectiveOptions.Tenant)
+ .ConfigureAwait(false);
+
+ HttpResponseMessage response;
+ using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(
+ effectiveOptions.HttpMethod,
+ effectiveOptions.GetApiUrl()))
+ {
+ httpRequestMessage.Headers.Add(
+ Constants.Authorization,
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} {1}",
+ Constants.Bearer,
+ accessToken));
+ response = await _httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs
new file mode 100644
index 000000000..a46c85bb8
--- /dev/null
+++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Identity.Web
+{
+ ///
+ /// Extension methods to support downstream web API services.
+ ///
+ public static class DownstreamWebApiServiceExtensions
+ {
+ ///
+ /// Adds a named downstream web API service related to a specific configuration section.
+ ///
+ /// Builder.
+ /// Name of the configuration for the service.
+ /// This is the name used when calling the service from controller/pages.
+ /// Configuration.
+ /// The builder for chaining.
+ public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddDownstreamWebApiService(
+ this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder,
+ string serviceName,
+ IConfiguration configuration)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Configure(serviceName, configuration);
+ builder.Services.AddHttpClient();
+ return builder;
+ }
+
+ ///
+ /// Adds a named downstream web API service initialized with delegates.
+ ///
+ /// Builder.
+ /// Name of the configuration for the service.
+ /// This is the name which will be used when calling the service from controller/pages.
+ /// Action to configure the options.
+ /// The builder for chaining.
+ public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddDownstreamWebApiService(
+ this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder,
+ string serviceName,
+ Action configureOptions)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Configure(serviceName, configureOptions);
+
+ // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
+ builder.Services.AddHttpClient();
+ builder.Services.Configure(serviceName, configureOptions);
+ return builder;
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiOptions.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiOptions.cs
new file mode 100644
index 000000000..eb9e3f154
--- /dev/null
+++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiOptions.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Net.Http;
+
+namespace Microsoft.Identity.Web
+{
+ ///
+ /// Options passed-in to call downstream web APIs. To call Microsoft Graph, see rather
+ /// .
+ ///
+ public class DownstreamWebApiOptions
+ {
+ ///
+ /// Base URL for the called downstream web API. For instance "https://graph.microsoft.com/beta/"..
+ ///
+ public string BaseUrl { get; set; } = "https://graph.microsoft.com/v1.0";
+
+ ///
+ /// Path relative to the (for instance "me").
+ ///
+ public string RelativePath { get; set; } = string.Empty;
+
+ ///
+ /// Space separated scopes required to call the downstream web API.
+ /// For instance "user.read mail.read".
+ ///
+ public string? Scopes { get; set; } = null;
+
+ ///
+ /// [Optional] tenant ID. This is used for specific scenarios where
+ /// the application needs to call a downstream web API on behalf of a user in several tenants.
+ /// It would mostly be used from code, not from the configuration.
+ ///
+ public string? Tenant { get; set; } = null;
+
+ ///
+ /// [Optional]. User flow (in the case of a B2C downstream web API). If not
+ /// specified, the B2C downstream web API will be called with the default user flow from
+ /// .
+ ///
+ public string? UserFlow { get; set; } = null;
+
+ ///
+ /// HTTP method used to call this downstream web API (by default Get).
+ ///
+ public HttpMethod HttpMethod { get; set; } = HttpMethod.Get;
+
+ ///
+ /// Clone the options (to be able to override them).
+ ///
+ /// A clone of the options.
+ public DownstreamWebApiOptions Clone()
+ {
+ return new DownstreamWebApiOptions
+ {
+ BaseUrl = BaseUrl,
+ RelativePath = RelativePath,
+ Scopes = Scopes,
+ Tenant = Tenant,
+ UserFlow = UserFlow,
+ HttpMethod = HttpMethod,
+ };
+ }
+
+ ///
+ /// Return the downstream web API URL.
+ ///
+ /// URL of the downstream web API.
+#pragma warning disable CA1055 // Uri return values should not be strings
+ public string GetApiUrl()
+#pragma warning restore CA1055 // Uri return values should not be strings
+ {
+ return BaseUrl?.TrimEnd('/') + $"/{RelativePath}";
+ }
+
+ ///
+ /// Returns the scopes.
+ ///
+ /// Scopes.
+ public string[] GetScopes()
+ {
+ return string.IsNullOrWhiteSpace(Scopes) ? new string[0] : Scopes.Split(' ');
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs
new file mode 100644
index 000000000..2c940a224
--- /dev/null
+++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Microsoft.Identity.Web
+{
+ ///
+ /// Interface used to call a downstream web API, for instance from controllers.
+ ///
+ public interface IDownstreamWebApi
+ {
+ ///
+ /// Calls the downstream web API for the user, based on a description of the
+ /// downstream web API in the configuration.
+ ///
+ /// Name of the service describing the downstream web API. There can
+ /// be several configuration named sections mapped to a ,
+ /// each for one downstream web API. You can pass-in null, but in that case
+ /// need to be set.
+ /// Overrides the options proposed in the configuration described
+ /// by .
+ /// [Optional] Claims representing a user. This is useful platforms like Blazor
+ /// or Azure Signal R where the HttpContext is not available. In other platforms, the library
+ /// will find the user from the HttpContext.
+ /// HTTP context in the case where is
+ /// , , .
+ /// An that the application will process.
+ public Task CallWebApiForUserAsync(
+ string serviceName,
+ Action? calledDownstreamWebApiOptionsOverride = null,
+ ClaimsPrincipal? user = null,
+ StringContent? content = null);
+
+ ///
+ /// Calls a downstream web API consuming JSON with some data and returns data.
+ ///
+ /// Input type.
+ /// Output type.
+ /// Name of the service describing the downstream web API. There can
+ /// be several configuration named sections mapped to a ,
+ /// each for one downstream web API. You can pass-in null, but in that case
+ /// need to be set.
+ /// Input parameter to the downstream web API.
+ /// Overrides the options proposed in the configuration described
+ /// by .
+ /// [Optional] Claims representing a user. This is useful in platforms like Blazor
+ /// or Azure Signal R where the HttpContext is not available. In other platforms, the library
+ /// will find the user from the HttpContext.
+ /// The value returned by the downstream web API.
+ ///
+ /// A list method that returns an IEnumerable<Todo>>.
+ ///
+ /// public async Task<IEnumerable<Todo>> GetAsync()
+ /// {
+ /// return await _downstreamWebApi.CallWebApiForUserAsync<object, IEnumerable<Todo>>(
+ /// ServiceName,
+ /// null,
+ /// options =>
+ /// {
+ /// options.RelativePath = $"api/todolist";
+ /// });
+ /// }
+ ///
+ ///
+ /// Example of editing.
+ ///
+ /// public async Task<Todo> EditAsync(Todo todo)
+ /// {
+ /// return await _downstreamWebApi.CallWebApiForUserAsync<Todo, Todo>(
+ /// ServiceName,
+ /// todo,
+ /// options =>
+ /// {
+ /// options.HttpMethod = HttpMethod.Patch;
+ /// options.RelativePath = $"api/todolist/{todo.Id}";
+ /// });
+ /// }
+ ///
+ ///
+ public Task CallWebApiForUserAsync(
+ string serviceName,
+ TInput input,
+ Action? downstreamWebApiOptionsOverride = null,
+ ClaimsPrincipal? user = null)
+ where TOutput : class;
+
+ ///
+ /// Calls the downstream web API for the app, with the required scopes.
+ ///
+ /// Name of the service describing the downstream web API. There can
+ /// be several configuration named sections mapped to a ,
+ /// each for one downstream web API. You can pass-in null, but in that case
+ /// need to be set.
+ /// Overrides the options proposed in the configuration described
+ /// by .
+ /// HTTP content in the case where is
+ /// , , .
+ /// An that the application will process.
+ public Task CallWebApiForAppAsync(
+ string serviceName,
+ Action? downstreamWebApiOptionsOverride = null,
+ StringContent? content = null);
+ }
+}
diff --git a/src/Microsoft.Identity.Web/GlobalSuppressions.cs b/src/Microsoft.Identity.Web/GlobalSuppressions.cs
new file mode 100644
index 000000000..be01584b6
--- /dev/null
+++ b/src/Microsoft.Identity.Web/GlobalSuppressions.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Design", "CA1055:Uri return values should not be strings", Justification = "We want a string.", Scope = "member", Target = "~M:Microsoft.Identity.Web.CalledApiOptions.GetApiUrl~System.String")]
diff --git a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml
index ab6114de2..ef89ca7e8 100644
--- a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml
+++ b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml
@@ -530,6 +530,204 @@
True, if the user agent does not allow "SameSite=None" cookie; otherwise, false.
+
+
+ Implementation for the downstream web API.
+
+
+
+
+ Constructor.
+
+ Token acquisition service.
+ Named options provider.
+ HTTP client.
+
+
+
+
+
+
+ Merge the options from configuration and override from caller.
+
+ Named configuration.
+ Delegate to override the configuration.
+
+
+
+
+
+
+
+
+
+ Extension methods to support downstream web API services.
+
+
+
+
+ Adds a named downstream web API service related to a specific configuration section.
+
+ Builder.
+ Name of the configuration for the service.
+ This is the name used when calling the service from controller/pages.
+ Configuration.
+ The builder for chaining.
+
+
+
+ Adds a named downstream web API service initialized with delegates.
+
+ Builder.
+ Name of the configuration for the service.
+ This is the name which will be used when calling the service from controller/pages.
+ Action to configure the options.
+ The builder for chaining.
+
+
+
+ Options passed-in to call downstream web APIs. To call Microsoft Graph, see rather
+ .
+
+
+
+
+ Base URL for the called downstream web API. For instance "https://graph.microsoft.com/beta/"..
+
+
+
+
+ Path relative to the (for instance "me").
+
+
+
+
+ Space separated scopes required to call the downstream web API.
+ For instance "user.read mail.read".
+
+
+
+
+ [Optional] tenant ID. This is used for specific scenarios where
+ the application needs to call a downstream web API on behalf of a user in several tenants.
+ It would mostly be used from code, not from the configuration.
+
+
+
+
+ [Optional]. User flow (in the case of a B2C downstream web API). If not
+ specified, the B2C downstream web API will be called with the default user flow from
+ .
+
+
+
+
+ HTTP method used to call this downstream web API (by default Get).
+
+
+
+
+ Clone the options (to be able to override them).
+
+ A clone of the options.
+
+
+
+ Return the downstream web API URL.
+
+ URL of the downstream web API.
+
+
+
+ Returns the scopes.
+
+ Scopes.
+
+
+
+ Interface used to call a downstream web API, for instance from controllers.
+
+
+
+
+ Calls the downstream web API for the user, based on a description of the
+ downstream web API in the configuration.
+
+ Name of the service describing the downstream web API. There can
+ be several configuration named sections mapped to a ,
+ each for one downstream web API. You can pass-in null, but in that case
+ need to be set.
+ Overrides the options proposed in the configuration described
+ by .
+ [Optional] Claims representing a user. This is useful platforms like Blazor
+ or Azure Signal R where the HttpContext is not available. In other platforms, the library
+ will find the user from the HttpContext.
+ HTTP context in the case where is
+ , , .
+ An that the application will process.
+
+
+
+ Calls a downstream web API consuming JSON with some data and returns data.
+
+ Input type.
+ Output type.
+ Name of the service describing the downstream web API. There can
+ be several configuration named sections mapped to a ,
+ each for one downstream web API. You can pass-in null, but in that case
+ need to be set.
+ Input parameter to the downstream web API.
+ Overrides the options proposed in the configuration described
+ by .
+ [Optional] Claims representing a user. This is useful in platforms like Blazor
+ or Azure Signal R where the HttpContext is not available. In other platforms, the library
+ will find the user from the HttpContext.
+ The value returned by the downstream web API.
+
+ A list method that returns an IEnumerable<Todo>>.
+
+ public async Task<IEnumerable<Todo>> GetAsync()
+ {
+ return await _downstreamWebApi.CallWebApiForUserAsync<object, IEnumerable<Todo>>(
+ ServiceName,
+ null,
+ options =>
+ {
+ options.RelativePath = $"api/todolist";
+ });
+ }
+
+
+ Example of editing.
+
+ public async Task<Todo> EditAsync(Todo todo)
+ {
+ return await _downstreamWebApi.CallWebApiForUserAsync<Todo, Todo>(
+ ServiceName,
+ todo,
+ options =>
+ {
+ options.HttpMethod = HttpMethod.Patch;
+ options.RelativePath = $"api/todolist/{todo.Id}";
+ });
+ }
+
+
+
+
+
+ Calls the downstream web API for the app, with the required scopes.
+
+ Name of the service describing the downstream web API. There can
+ be several configuration named sections mapped to a ,
+ each for one downstream web API. You can pass-in null, but in that case
+ need to be set.
+ Overrides the options proposed in the configuration described
+ by .
+ HTTP content in the case where is
+ , , .
+ An that the application will process.
+
Extension methods.
@@ -747,30 +945,30 @@
- Extensions methods on a MicrososoftAppCallingWebApiAuthenticationBuilder builder
+ Extensions methods on a MicrosoftIdentityAppCallingWebApiAuthenticationBuilder builder
to add support to call Microsoft Graph.
- Add support to calls Microsoft graph. From a named option and a configuration section.
+ Add support to call Microsoft Graph. From a named option and a configuration section.
Builder.
- Configuraiton section.
+ Configuration section.
The builder to chain.
- Add support to calls Microsoft graph. From a base graph Url and a default scope.
+ Add support to call Microsoft Graph. From a base Graph URL and a default scope.
Builder.
Named instance of option.
- Configuraiton section.
+ Configuration section.
The builder to chain.
- Add support to calls Microsoft graph. From a named options and a configuraiton method.
+ Add support to call Microsoft Graph. From a named options and a configuration method.
Builder.
Method to configure the options.
@@ -778,7 +976,7 @@
- Authentication provider based on MSAL.NET.
+ Authentication provider based on ITokenAcquisition.
diff --git a/tests/BlazorServerCallsGraph/Pages/CallWebApi.razor b/tests/BlazorServerCallsGraph/Pages/CallWebApi.razor
index 6b06cde64..27e7b6892 100644
--- a/tests/BlazorServerCallsGraph/Pages/CallWebApi.razor
+++ b/tests/BlazorServerCallsGraph/Pages/CallWebApi.razor
@@ -20,13 +20,25 @@ else
}
@code {
+ private HttpResponseMessage response;
private string apiResult;
protected override async Task OnInitializedAsync()
{
try
{
- apiResult = await downstreamAPI.CallWebApiAsync("me");
+ response = await downstreamAPI.CallWebApiForUserAsync(
+ "CalledApi",
+ options => options.RelativePath = "me");
+
+ if (response.StatusCode == System.Net.HttpStatusCode.OK)
+ {
+ apiResult = await response.Content.ReadAsStringAsync();
+ }
+ else
+ {
+ apiResult = "Failed to call the web API";
+ }
}
catch (Exception ex)
{
diff --git a/tests/BlazorServerCallsGraph/Services/DownstreamWebApi.cs b/tests/BlazorServerCallsGraph/Services/DownstreamWebApi.cs
deleted file mode 100644
index 26646081f..000000000
--- a/tests/BlazorServerCallsGraph/Services/DownstreamWebApi.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Identity.Web;
-
-namespace blazor
-{
- public interface IDownstreamWebApi
- {
- Task CallWebApiAsync(string relativeEndpoint = "", string[] requiredScopes = null);
- }
-
- public static class DownstreamWebApiExtensions
- {
- public static void AddDownstreamWebApiService(this IServiceCollection services, IConfiguration configuration)
- {
- // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
- services.AddHttpClient();
- }
- }
-
- public class DownstreamWebApi : IDownstreamWebApi
- {
- private readonly ITokenAcquisition _tokenAcquisition;
-
- private readonly IConfiguration _configuration;
-
- private readonly HttpClient _httpClient;
-
- public DownstreamWebApi(
- ITokenAcquisition tokenAcquisition,
- IConfiguration configuration,
- HttpClient httpClient)
- {
- _tokenAcquisition = tokenAcquisition;
- _configuration = configuration;
- _httpClient = httpClient;
- }
-
- ///
- /// Calls the Web API with the required scopes
- ///
- /// [Optional] Scopes required to call the Web API. If
- /// not specified, uses scopes from the configuration
- /// Endpoint relative to the CalledApiUrl configuration
- /// A JSON string representing the result of calling the Web API
- public async Task CallWebApiAsync(string relativeEndpoint = "", string[] requiredScopes = null)
- {
- string[] scopes = requiredScopes ?? _configuration["CalledApi:CalledApiScopes"]?.Split(' ');
- string apiUrl = (_configuration["CalledApi:CalledApiUrl"] as string)?.TrimEnd('/') + $"/{relativeEndpoint}";
-
- string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
- HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, apiUrl);
- httpRequestMessage.Headers.Add("Authorization", $"bearer {accessToken}");
-
- string apiResult;
- var response = await _httpClient.SendAsync(httpRequestMessage);
- if (response.StatusCode == HttpStatusCode.OK)
- {
- apiResult = await response.Content.ReadAsStringAsync();
- }
- else
- {
- apiResult = $"Error calling the API '{apiUrl}'";
- }
-
- return apiResult;
- }
- }
-}
diff --git a/tests/BlazorServerCallsGraph/Startup.cs b/tests/BlazorServerCallsGraph/Startup.cs
index 70772ff4e..89e365e5e 100644
--- a/tests/BlazorServerCallsGraph/Startup.cs
+++ b/tests/BlazorServerCallsGraph/Startup.cs
@@ -30,9 +30,9 @@ public void ConfigureServices(IServiceCollection services)
.AddMicrosoftIdentityWebApp(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraphServiceClient(Configuration.GetSection("GraphBeta"))
+ .AddDownstreamWebApiService("CalledApi", Configuration.GetSection("CalledApi"))
.AddInMemoryTokenCaches();
- services.AddDownstreamWebApiService(Configuration);
services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
diff --git a/tests/BlazorServerCallsGraph/appsettings.json b/tests/BlazorServerCallsGraph/appsettings.json
index ea0cda6c5..bb607fb60 100644
--- a/tests/BlazorServerCallsGraph/appsettings.json
+++ b/tests/BlazorServerCallsGraph/appsettings.json
@@ -22,15 +22,15 @@
App ID URI of a legacy v1 Web application
Applications are registered in the https:portal.azure.com portal.
*/
- "CalledApiScopes": "user.read",
- "CalledApiUrl": "https://graph.microsoft.com/beta/"
- },
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
- }
- },
- "AllowedHosts": "*"
+ "Scopes": "user.read",
+ "BaseUrl": "https://graph.microsoft.com/v1.0/"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
}
diff --git a/tests/WebAppCallsWebApiCallsGraph/Client/Controllers/TodoListController.cs b/tests/WebAppCallsWebApiCallsGraph/Client/Controllers/TodoListController.cs
index 2c86cdc47..653b22f74 100644
--- a/tests/WebAppCallsWebApiCallsGraph/Client/Controllers/TodoListController.cs
+++ b/tests/WebAppCallsWebApiCallsGraph/Client/Controllers/TodoListController.cs
@@ -1,35 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
+using System.Collections.Generic;
+using System.Net.Http;
using System.Threading.Tasks;
-using TodoListClient.Services;
using TodoListService.Models;
namespace TodoListClient.Controllers
{
+ [Authorize]
+ [AuthorizeForScopes(ScopeKeySection = "TodoList:Scopes")]
public class TodoListController : Controller
{
- private ITodoListService _todoListService;
+ private IDownstreamWebApi _downstreamWebApi;
+ private const string ServiceName = "TodoList";
- public TodoListController(ITodoListService todoListService)
+ public TodoListController(IDownstreamWebApi downstreamWebApi)
{
- _todoListService = todoListService;
+ _downstreamWebApi = downstreamWebApi;
}
// GET: TodoList
- [AuthorizeForScopes(ScopeKeySection = "TodoList:TodoListScope")]
public async Task Index()
{
- return View(await _todoListService.GetAsync());
+ var value = await _downstreamWebApi.CallWebApiForUserAsync