Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Smaller) PR just for the DownstreamApiService on top of the Graph service #447

Merged
merged 40 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2cf1c9a
Initial commit (does not build)
jmprieur Aug 3, 2020
b6dcf59
Updating Web API.
jmprieur Aug 4, 2020
fdc7004
Improving the API
jmprieur Aug 4, 2020
4d3e535
Improving the API.
jmprieur Aug 4, 2020
96d643e
Updating unit tests so that they build
jmprieur Aug 4, 2020
d81a3cb
Merge branch 'master' into jmprieur/WIPNewApi
jmprieur Aug 4, 2020
aa71f45
Renamings an clean-up discussed with DevDiv
jmprieur Aug 5, 2020
f84bc4b
More renaming
jmprieur Aug 5, 2020
2f7ad84
Test fix.
pmaytak Aug 5, 2020
e2efa7c
Renaming of more overrides
jmprieur Aug 8, 2020
2c8c092
Updating the templates
jmprieur Aug 8, 2020
e7a3c10
initial commit api w/microsoftIdentity
jennyf19 Aug 9, 2020
77de4a0
merge conflict
jennyf19 Aug 9, 2020
6bc76ba
merge conflicts + update templates
jennyf19 Aug 9, 2020
7d56096
Merge from master
jmprieur Aug 9, 2020
83a2921
Making the templates work with 0.3.*-*
jmprieur Aug 9, 2020
d5c42d9
Merge branch 'jmprieur/WIPNewApi' into jennyf/newAPI
jennyf19 Aug 10, 2020
242e5c4
few more updates
jennyf19 Aug 10, 2020
e493c5a
Make GetTokenForAppAsync less confusing and allow to pass tenantId #4…
jmprieur Aug 10, 2020
eb198b2
fix tests
jennyf19 Aug 10, 2020
843d416
add xml comments
jennyf19 Aug 10, 2020
d71597a
Merge branch 'jmprieur/WIPNewApi' into jennyf/newAPI
jennyf19 Aug 10, 2020
01fd179
few spelling changes
jennyf19 Aug 11, 2020
e6f346b
renaming of CallsWebApi to EnableTokenAcquisitionToCallDownstreamApi
jennyf19 Aug 11, 2020
5283a23
merge conflict
jennyf19 Aug 11, 2020
fbffb71
- Adding a missing renaming for AddMicrosoftWebApp => AddMicrosoftIde…
jmprieur Aug 12, 2020
3c71db7
Fixing the TodoListService controller in WebAppCallsWebApiCallsGraph
jmprieur Aug 12, 2020
ee38779
Smaller PR for MicrosoftGraphClientService
jmprieur Aug 12, 2020
af2f9fa
Merge branch 'jennyf/newAPI' into jennyf/newApiPlusGraphService
jmprieur Aug 12, 2020
14e1f35
Merge from master
jmprieur Aug 12, 2020
789fd51
fix PR feedback for xml comments and constants (#442)
jennyf19 Aug 13, 2020
0aed39a
Addressing PR feedback
jmprieur Aug 13, 2020
ba40cf3
Merge branch 'jennyf/newApiPlusGraphService' of https://github.com/Az…
jmprieur Aug 13, 2020
7194065
Merge branch 'master' into jennyf/newApiPlusGraphService
jmprieur Aug 13, 2020
e1edd34
Branch with less changes, just for the DownstreamApiService
jmprieur Aug 13, 2020
c1688f8
Update src/Microsoft.Identity.Web/DownstreamApiSupport/IDownstreamWeb…
jmprieur Aug 13, 2020
6c0c880
Fixes a XML comment
jmprieur Aug 13, 2020
970d438
Merge branch 'jennyf/newApiPlusGraphServicePlusDownstreamApiService' …
jmprieur Aug 13, 2020
0ec9bbf
Merge from master
jmprieur Aug 13, 2020
3989e4e
update sample to use downstream api & some spelling fixes (#449)
jennyf19 Aug 13, 2020
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
1 change: 1 addition & 0 deletions src/Microsoft.Identity.Web/Constants/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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=";
Expand Down
4 changes: 3 additions & 1 deletion src/Microsoft.Identity.Web/Constants/IDWebErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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'. ";
Expand All @@ -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. ";
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Implementation for the downstream web API.
/// </summary>
public class DownstreamWebApi : IDownstreamWebApi
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly HttpClient _httpClient;
private readonly IOptionsMonitor<DownstreamWebApiOptions> _namedDownstreamWebApiOptions;
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};

/// <summary>
/// Constructor.
/// </summary>
/// <param name="tokenAcquisition">Token acquisition service.</param>
/// <param name="namedDownstreamWebApiOptions">Named options provider.</param>
/// <param name="httpClient">HTTP client.</param>
public DownstreamWebApi(
ITokenAcquisition tokenAcquisition,
IOptionsMonitor<DownstreamWebApiOptions> namedDownstreamWebApiOptions,
HttpClient httpClient)
{
_tokenAcquisition = tokenAcquisition;
_namedDownstreamWebApiOptions = namedDownstreamWebApiOptions;
_httpClient = httpClient;
}

/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForUserAsync(
string optionsInstanceName,
Action<DownstreamWebApiOptions>? 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;
}

/// <summary>
/// Merge the options from configuration and override from caller.
/// </summary>
/// <param name="optionsInstanceName">Named configuration.</param>
/// <param name="calledApiOptionsOverride">Delegate to override the configuration.</param>
internal /* for tests */ DownstreamWebApiOptions MergeOptions(
string optionsInstanceName,
Action<DownstreamWebApiOptions>? 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;
}

/// <inheritdoc/>
public async Task<TOutput?> CallWebApiForUserAsync<TInput, TOutput>(
string optionsInstanceName,
TInput input,
Action<DownstreamWebApiOptions>? 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<TOutput>(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));
}
}

/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForAppAsync(
string optionsInstanceName,
Action<DownstreamWebApiOptions>? 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extension methods to support downstream web API services.
/// </summary>
public static class DownstreamWebApiServiceExtensions
{
/// <summary>
/// Adds a named downstream web API service related to a specific configuration section.
/// </summary>
/// <param name="builder">Builder.</param>
/// <param name="serviceName">Name of the configuration for the service.
/// This is the name used when calling the service from controller/pages.</param>
/// <param name="configuration">Configuration.</param>
/// <returns>The builder for chaining.</returns>
public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddDownstreamWebApiService(
this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder,
string serviceName,
IConfiguration configuration)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Services.Configure<DownstreamWebApiOptions>(serviceName, configuration);
builder.Services.AddHttpClient<IDownstreamWebApi, DownstreamWebApi>();
return builder;
}

/// <summary>
/// Adds a named downstream web API service initialized with delegates.
/// </summary>
/// <param name="builder">Builder.</param>
/// <param name="serviceName">Name of the configuration for the service.
/// This is the name which will be used when calling the service from controller/pages.</param>
/// <param name="configureOptions">Action to configure the options.</param>
/// <returns>The builder for chaining.</returns>
public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddDownstreamWebApiService(
this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder,
string serviceName,
Action<DownstreamWebApiOptions> configureOptions)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Services.Configure<DownstreamWebApiOptions>(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<IDownstreamWebApi, DownstreamWebApi>();
builder.Services.Configure(serviceName, configureOptions);
return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Net.Http;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Options passed-in to call downstream web APIs. To call Microsoft Graph, see rather
/// <see cref="MicrosoftGraphOptions"/>.
/// </summary>
public class DownstreamWebApiOptions
{
/// <summary>
/// Base URL for the called downstream web API. For instance <c>"https://graph.microsoft.com/beta/".</c>.
/// </summary>
public string BaseUrl { get; set; } = "https://graph.microsoft.com/v1.0";

/// <summary>
/// Path relative to the <see cref="BaseUrl"/> (for instance "me").
/// </summary>
public string RelativePath { get; set; } = string.Empty;

/// <summary>
/// Space separated scopes required to call the downstream web API.
/// For instance "user.read mail.read".
/// </summary>
public string? Scopes { get; set; } = null;

/// <summary>
/// [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.
/// </summary>
public string? Tenant { get; set; } = null;

/// <summary>
/// [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
/// <see cref="MicrosoftIdentityOptions.DefaultUserFlow"/>.
/// </summary>
public string? UserFlow { get; set; } = null;

/// <summary>
/// HTTP method used to call this downstream web API (by default Get).
/// </summary>
public HttpMethod HttpMethod { get; set; } = HttpMethod.Get;

/// <summary>
/// Clone the options (to be able to override them).
/// </summary>
/// <returns>A clone of the options.</returns>
public DownstreamWebApiOptions Clone()
{
return new DownstreamWebApiOptions
{
BaseUrl = BaseUrl,
RelativePath = RelativePath,
Scopes = Scopes,
Tenant = Tenant,
UserFlow = UserFlow,
HttpMethod = HttpMethod,
};
}

/// <summary>
/// Return the downstream web API URL.
/// </summary>
/// <returns>URL of the downstream web API.</returns>
#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}";
}

/// <summary>
/// Returns the scopes.
/// </summary>
/// <returns>Scopes.</returns>
public string[] GetScopes()
{
return string.IsNullOrWhiteSpace(Scopes) ? new string[0] : Scopes.Split(' ');
}
}
}
Loading