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 35 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
7 changes: 6 additions & 1 deletion src/Microsoft.Identity.Web/Constants/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ internal static class Constants
public const string Bearer = "Bearer";
public const string LoginHint = "loginHint";
public const string DomainHint = "domainHint";
public const string Authorization = "Authorization";

// Blazor challenge uri
// Blazor challenge URI
public const string BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=";

// Microsoft Graph
public const string UserReadScope = "user.read";
public const string GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal static class IDWebErrorMessage
public const string UnauthenticatedUser = "IDW10204:The user is unauthenticated. The HttpContext does not contain any claims. ";
public const string BlazorServerBaseUriNotSet = "IDW10205: Using Blazor server but the base URI was not properly set. ";
public const string BlazorServerUserNotSet = "IDW10206: Using Blazor server but the user was not properly set. ";
public const string CalledApiScopesAreNull = "IDW10207: The CalledApiScopes cannot be null. ";

// Token Validation IDW10300 = "IDW10300:"
public const string IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. ";
Expand Down
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 web APIs. To call Microsoft Graph, see rather
/// <see cref="MicrosoftGraphOptions"/>.
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public class DownstreamApiOptions
{
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Base URL for the called 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 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 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 Web API). If not
/// specified, the B2C web API will be called with the default user flow from
/// <see cref="MicrosoftIdentityOptions.DefaultUserFlow"/>.
/// </summary>
public string? UserFlow { get; set; } = null;

jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Http method used to call this 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 DownstreamApiOptions Clone()
{
return new DownstreamApiOptions
{
BaseUrl = BaseUrl,
RelativePath = RelativePath,
Scopes = Scopes,
Tenant = Tenant,
UserFlow = UserFlow,
HttpMethod = HttpMethod,
};
}

/// <summary>
/// Return the Api URL.
/// </summary>
/// <returns>URL of the 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(' ');
}
}
}
172 changes: 172 additions & 0 deletions src/Microsoft.Identity.Web/DownstreamApiSupport/DownstreamWebApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
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.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Graph;
using Microsoft.Identity.Web;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Implementation for the downstream API.
/// </summary>
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
public class DownstreamWebApi : IDownstreamWebApi
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly HttpClient _httpClient;
private readonly IOptionsMonitor<DownstreamApiOptions> _namedOptions;
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};

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

/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForUserAsync(
string optionsInstanceName,
Action<DownstreamApiOptions>? calledApiOptionsOverride,
ClaimsPrincipal? user,
StringContent? requestContent)
{
DownstreamApiOptions effectiveOptions = MergeOptions(optionsInstanceName, calledApiOptionsOverride);

// verify scopes is not null
if (string.IsNullOrEmpty(effectiveOptions.Scopes))
{
throw new ArgumentException("Scopes need to be passed-in either by configuration or by the delegate overring it.");
}
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved

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("Authorization", $"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 */ DownstreamApiOptions MergeOptions(
string optionsInstanceName,
Action<DownstreamApiOptions>? calledApiOptionsOverride)
{
// Gets the options from configuration (or default value)
DownstreamApiOptions options;
if (optionsInstanceName != null)
{
options = _namedOptions.Get(optionsInstanceName);
}
else
{
options = _namedOptions.CurrentValue;
}

// Give a chance to the called to override defaults for this call
DownstreamApiOptions clonedOptions = options.Clone();
calledApiOptionsOverride?.Invoke(clonedOptions);
return clonedOptions;
}

/// <inheritdoc/>
public async Task<TOutput?> CallWebApiForUserAsync<TInput, TOutput>(
string optionsInstanceName,
TInput input,
Action<DownstreamApiOptions>? downstreamApiOptionsOverride = null,
ClaimsPrincipal? user = null)
where TOutput : class
{
StringContent? jsoncontent;
if (input != null)
{
var jsonRequest = JsonSerializer.Serialize(input);
jsoncontent = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// case of patch? jsoncontent = new StringContent(jsonRequest, Encoding.UTF8, "application/json-patch+json");
}
else
{
jsoncontent = null;
}

HttpResponseMessage response = await CallWebApiForUserAsync(
optionsInstanceName,
downstreamApiOptionsOverride,
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($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}");
}
}

/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForAppAsync(
string optionsInstanceName,
Action<DownstreamApiOptions>? downstreamApiOptionsOverride = null,
StringContent? requestContent = null)
{
DownstreamApiOptions effectiveOptions = MergeOptions(optionsInstanceName, downstreamApiOptionsOverride);

if (effectiveOptions.Scopes == null)
{
throw new ArgumentException("Scopes need to be passed-in either by configuration or by the delegate overring it.");
}

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("Authorization", $"bearer {accessToken}");
response = await _httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
}

return response;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extension methods to support downstream api services.
/// </summary>
public static class DownstreamApiServiceExtensions
{
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Adds a named downstream API service related to a specific configuration section.
/// </summary>
/// <param name="builder">Builder.</param>
/// <param name="optionsInstanceName">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="configuration">Configuration.</param>
/// <returns>The builder for chaining.</returns>
public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddDownstreamApiService(
this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder,
string optionsInstanceName,
IConfiguration configuration)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

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

/// <summary>
/// Adds a named downstream API service initialized with delegates.
/// </summary>
/// <param name="builder">Builder.</param>
/// <param name="optionsInstanceName">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 AddDownstreamApiService(
this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder,
string optionsInstanceName,
Action<DownstreamApiOptions> configureOptions)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Services.Configure<DownstreamApiOptions>(optionsInstanceName, 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(optionsInstanceName, configureOptions);
return builder;
}
}
}
Loading