Skip to content

Commit

Permalink
Add support for Microsoft.Graph 5.x (AzureAD#2276)
Browse files Browse the repository at this point in the history
* Spec-in dev ex with Graph 5
* add graph beta support (AzureAD#2273)
* fix sample (AzureAD#2278)
* Fixing the tests
* Adding GraphServiceClient modifiers (WithAppOnly, WithSCopes, ....) (AzureAD#2279)
* Updating the README.md
---------
Co-authored-by: jennyf19 <[email protected]>
  • Loading branch information
jmprieur authored Jun 13, 2023
1 parent 14ff26f commit 0e836ab
Show file tree
Hide file tree
Showing 24 changed files with 936 additions and 15 deletions.
21 changes: 21 additions & 0 deletions Microsoft.Identity.Web.sln
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DaemonConsoleCallingDownstr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenAcquirerTests", "tests\IntegrationTests\TokenAcquirerTests\TokenAcquirerTests.csproj", "{AB8177BE-8961-4698-B064-42943885D48D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.GraphServiceClient", "src\Microsoft.Identity.Web.GraphServiceClient\Microsoft.Identity.Web.GraphServiceClient.csproj", "{4DF02DF7-D092-4F45-8892-8A1D3E612706}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.GraphServiceClientBeta", "src\Microsoft.Identity.Web.GraphServiceClientBeta\Microsoft.Identity.Web.GraphServiceClientBeta.csproj", "{608F0E0B-A52D-4E0F-9B1A-BA9BDA866484}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphServiceClientTests", "tests\IntegrationTests\GraphServiceClientTests\GraphServiceClientTests.csproj", "{F686A507-CAC6-4349-9112-27F5AEFBF12B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -373,6 +379,18 @@ Global
{AB8177BE-8961-4698-B064-42943885D48D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB8177BE-8961-4698-B064-42943885D48D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB8177BE-8961-4698-B064-42943885D48D}.Release|Any CPU.Build.0 = Release|Any CPU
{4DF02DF7-D092-4F45-8892-8A1D3E612706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DF02DF7-D092-4F45-8892-8A1D3E612706}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DF02DF7-D092-4F45-8892-8A1D3E612706}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DF02DF7-D092-4F45-8892-8A1D3E612706}.Release|Any CPU.Build.0 = Release|Any CPU
{608F0E0B-A52D-4E0F-9B1A-BA9BDA866484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{608F0E0B-A52D-4E0F-9B1A-BA9BDA866484}.Debug|Any CPU.Build.0 = Debug|Any CPU
{608F0E0B-A52D-4E0F-9B1A-BA9BDA866484}.Release|Any CPU.ActiveCfg = Release|Any CPU
{608F0E0B-A52D-4E0F-9B1A-BA9BDA866484}.Release|Any CPU.Build.0 = Release|Any CPU
{F686A507-CAC6-4349-9112-27F5AEFBF12B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F686A507-CAC6-4349-9112-27F5AEFBF12B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F686A507-CAC6-4349-9112-27F5AEFBF12B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F686A507-CAC6-4349-9112-27F5AEFBF12B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -448,6 +466,9 @@ Global
{D12AF43A-72EC-4459-B6F4-0755190D9222} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F}
{5DE68949-118E-4A2E-A541-8FB5CA030CD6} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F}
{AB8177BE-8961-4698-B064-42943885D48D} = {A7B1AE31-4E89-42A0-8264-FBEA795AB7D2}
{4DF02DF7-D092-4F45-8892-8A1D3E612706} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
{608F0E0B-A52D-4E0F-9B1A-BA9BDA866484} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
{F686A507-CAC6-4349-9112-27F5AEFBF12B} = {A7B1AE31-4E89-42A0-8264-FBEA795AB7D2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.CertificateLess, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.DownstreamApi, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.MicrosoftGraph, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.GraphServiceClient, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.GraphServiceClientBeta, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.MicrosoftGraphBeta, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.TokenAcquisition, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
[assembly: InternalsVisibleTo("Microsoft.Identity.Web.TokenCache, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Kiota.Abstractions;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Authentication options controlling the authentication request to the Microsoft Graph service.
/// </summary>
public class GraphAuthenticationOptions : GraphServiceClientOptions, IRequestOption
{
/// <summary>
/// Base URL for the Microsoft Graph API. By default: <c>"https://graph.microsoft.com/v1.0/"</c>
/// </summary>
public new string BaseUrl { get { return base.BaseUrl!; } }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Abstractions;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Authentication provider for Microsoft Graph, based on IAuthorizationHeaderProvider. This is richer
/// than <see cref="BaseBearerTokenAuthenticationProvider"/> which only supports the bearer protocol.
/// </summary>
internal class GraphAuthenticationProvider : IAuthenticationProvider
{
const string ScopeKey = "scopes";
private const string AuthorizationHeaderKey = "Authorization";
private const string AuthorizationHeaderProviderOptionsKey = "authorizationHeaderProviderOptions";
readonly IAuthorizationHeaderProvider _authorizationHeaderProvider;
readonly GraphServiceClientOptions _defaultAuthenticationOptions;

/// <summary>
/// Constructor from the authorization header provider.
/// </summary>
/// <param name="authorizationHeaderProvider"></param>
/// <param name="defaultAuthenticationOptions"></param>
public GraphAuthenticationProvider(IAuthorizationHeaderProvider authorizationHeaderProvider,
GraphServiceClientOptions defaultAuthenticationOptions)
{
_authorizationHeaderProvider = authorizationHeaderProvider;
_defaultAuthenticationOptions = defaultAuthenticationOptions;
}

/// <summary>
/// Method that performs the authentication, and adds the right headers to the request.
/// </summary>
/// <param name="request"></param>
/// <param name="additionalAuthenticationContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public async Task AuthenticateRequestAsync(
RequestInformation request,
Dictionary<string, object>? additionalAuthenticationContext = null,
CancellationToken cancellationToken = default)
{
_ = Throws.IfNull(request);

// Attempts to get the scopes
IEnumerable<string>? scopes;
GraphServiceClientOptions? graphServiceClientOptions;

if (additionalAuthenticationContext != null)
{
scopes = additionalAuthenticationContext.ContainsKey(ScopeKey) ? (string[])additionalAuthenticationContext[ScopeKey] : _defaultAuthenticationOptions?.Scopes;
graphServiceClientOptions = additionalAuthenticationContext.ContainsKey(AuthorizationHeaderProviderOptionsKey) ?
(GraphServiceClientOptions)additionalAuthenticationContext[AuthorizationHeaderProviderOptionsKey] :
_defaultAuthenticationOptions;
}
else
{
scopes = _defaultAuthenticationOptions.Scopes;
graphServiceClientOptions = _defaultAuthenticationOptions;
}

// Remove the authorization header if it exists
if (request.Headers.ContainsKey(AuthorizationHeaderKey))
{
request.Headers.Remove(AuthorizationHeaderKey);
}

// Data coming from the request (needed in protocols like "Pop")
AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions;
if (string.Compare(graphServiceClientOptions?.ProtocolScheme, "bearer", StringComparison.OrdinalIgnoreCase) == 0)
{
authorizationHeaderProviderOptions = new AuthorizationHeaderProviderOptions(graphServiceClientOptions!);
authorizationHeaderProviderOptions.BaseUrl = request.URI.Host;
authorizationHeaderProviderOptions.RelativePath = request.URI.LocalPath;
authorizationHeaderProviderOptions.HttpMethod = GetHttpMethod(request.HttpMethod);
}
else
{
authorizationHeaderProviderOptions = graphServiceClientOptions;
}

// Add the authorization header
if (!request.Headers.ContainsKey(AuthorizationHeaderKey))
{
string authorizationHeader;
if (authorizationHeaderProviderOptions!.RequestAppToken)
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(scopes!.FirstOrDefault()!,
authorizationHeaderProviderOptions);
}
else
{
authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(
scopes!,
authorizationHeaderProviderOptions);
}
request.Headers.Add(AuthorizationHeaderKey, authorizationHeader);
}
}

/// <summary>
/// Transforms the Kiota HTTP Method (enum) into a .NET HttpMethod (static members).
/// </summary>
/// <param name="httpMethod">Kiota Http method</param>
/// <returns>HttpMethod</returns>
private HttpMethod GetHttpMethod(Method httpMethod)
{
switch (httpMethod)
{
case Method.GET:
return HttpMethod.Get;
case Method.POST:
return HttpMethod.Post;
case Method.PUT:
return HttpMethod.Put;
case Method.PATCH:
#if NETFRAMEWORK || NETSTANDARD2_0
return HttpMethod.Put;
#else
return HttpMethod.Patch;
#endif
case Method.DELETE:
return HttpMethod.Delete;
case Method.OPTIONS:
return HttpMethod.Options;
case Method.TRACE:
return HttpMethod.Trace;
case Method.HEAD:
return HttpMethod.Head;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Identity.Abstractions;
using System.Collections.Generic;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Options passed-in to call Microsoft Graph.
/// </summary>
public class GraphServiceClientOptions : AuthorizationHeaderProviderOptions
{
/// <summary>
/// Options used to configure the authentication provider for Microsoft Graph.
/// </summary>
public GraphServiceClientOptions()
{
BaseUrl = Constants.GraphBaseUrlV1;
Scopes = new[] { Constants.UserReadScope };
}

/// <summary>
/// Scopes required to call the downstream web API.
/// For instance "user.read mail.read".
/// For Microsoft identity, in the case of application tokens (token
/// requested by the app on behalf of itself), there should be only one scope, and it
/// should end in "./default")
/// </summary>
public IEnumerable<string> Scopes { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Net.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Graph;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extensions methods on a MicrosoftIdentityAppCallingWebApiAuthenticationBuilder builder
/// to add support to call Microsoft Graph.
/// </summary>
public static class GraphServiceCollectionExtensions
{
/// <summary>
/// Add support to call Microsoft Graph. From a named option and a configuration section.
/// </summary>
/// <param name="services">Builder.</param>
/// <returns>The service collection to chain.</returns>
public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services)
{
services.AddTokenAcquisition();
services.AddHttpClient();
return services.AddMicrosoftGraph(options => { });
}

/// <summary>
/// Add support to call Microsoft Graph. From a base Graph URL and a default scope.
/// </summary>
/// <param name="services">Builder.</param>
/// <param name="configurationSection">Configuration section containing the Microsoft graph config.</param>
/// <returns>The service collection to chain.</returns>
public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services, IConfiguration configurationSection)
{
return services.AddMicrosoftGraph(o => configurationSection.Bind(o));
}

/// <summary>
/// Add support to call Microsoft Graph. From a base Graph URL and a default scope.
/// </summary>
/// <param name="services">Builder.</param>
/// <param name="configureMicrosoftGraphOptions">Delegate to configure the graph service options</param>
/// <returns>The service collection to chain.</returns>
public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services, Action<GraphServiceClientOptions> configureMicrosoftGraphOptions)
{
// https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
services.AddOptions<GraphServiceClientOptions>().Configure(configureMicrosoftGraphOptions);

services.AddScoped<GraphServiceClient, GraphServiceClient>(serviceProvider =>
{
var authorizationHeaderProvider = serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();
var options = serviceProvider.GetRequiredService<IOptions<GraphServiceClientOptions>>();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var microsoftGraphOptions = options.Value;
if (microsoftGraphOptions.Scopes == null)
{
Throws.ArgumentNullException("scopes", IDWebErrorMessage.CalledApiScopesAreNull);
}

var httpClient = httpClientFactory.CreateClient("GraphServiceClient");

GraphServiceClient graphServiceClient = new(httpClient,
new GraphAuthenticationProvider(authorizationHeaderProvider, microsoftGraphOptions), microsoftGraphOptions.BaseUrl);
return graphServiceClient;
});
return services;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>

<Title>Microsoft Identity Web, Microsoft Graph v5+ helper</Title>
<Product>Microsoft Identity Web</Product>
<Description>
This package enables ASP.NET Core web apps and web APIs to use the Microsoft identity platform (formerly Azure AD v2.0).
This package is specifically used for web applications, which sign-in users and call Microsoft Graph, and for protected web APIs
that call Microsoft Graph. Works specifically with MS Graph SDK v5 and above. For MS Graph SDK v4 support, please use Microsoft.Identity.Web.MicrosoftGraph.
</Description>
<ProjectGuid>{4DF02DF7-D092-4F45-8892-8A1D3E612706}</ProjectGuid>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Graph" Version="5.12.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Identity.Web.TokenAcquisition\Microsoft.Identity.Web.TokenAcquisition.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 0e836ab

Please sign in to comment.