diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln index a073a2192..468f7bd45 100644 --- a/Microsoft.Identity.Web.sln +++ b/Microsoft.Identity.Web.sln @@ -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 @@ -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 @@ -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} diff --git a/src/Microsoft.Identity.Web.Diagnostics/Properties/InternalsVisibleTo.cs b/src/Microsoft.Identity.Web.Diagnostics/Properties/InternalsVisibleTo.cs index 47a95dfa6..50cad359f 100644 --- a/src/Microsoft.Identity.Web.Diagnostics/Properties/InternalsVisibleTo.cs +++ b/src/Microsoft.Identity.Web.Diagnostics/Properties/InternalsVisibleTo.cs @@ -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")] diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/GraphAuthenticationOptions.cs b/src/Microsoft.Identity.Web.GraphServiceClient/GraphAuthenticationOptions.cs new file mode 100644 index 000000000..70c0affd0 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/GraphAuthenticationOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Kiota.Abstractions; + +namespace Microsoft.Identity.Web +{ + /// + /// Authentication options controlling the authentication request to the Microsoft Graph service. + /// + public class GraphAuthenticationOptions : GraphServiceClientOptions, IRequestOption + { + /// + /// Base URL for the Microsoft Graph API. By default: "https://graph.microsoft.com/v1.0/" + /// + public new string BaseUrl { get { return base.BaseUrl!; } } + } +} diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/GraphAuthenticationProvider.cs b/src/Microsoft.Identity.Web.GraphServiceClient/GraphAuthenticationProvider.cs new file mode 100644 index 000000000..4725c1cb2 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/GraphAuthenticationProvider.cs @@ -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 +{ + /// + /// Authentication provider for Microsoft Graph, based on IAuthorizationHeaderProvider. This is richer + /// than which only supports the bearer protocol. + /// + internal class GraphAuthenticationProvider : IAuthenticationProvider + { + const string ScopeKey = "scopes"; + private const string AuthorizationHeaderKey = "Authorization"; + private const string AuthorizationHeaderProviderOptionsKey = "authorizationHeaderProviderOptions"; + readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; + readonly GraphServiceClientOptions _defaultAuthenticationOptions; + + /// + /// Constructor from the authorization header provider. + /// + /// + /// + public GraphAuthenticationProvider(IAuthorizationHeaderProvider authorizationHeaderProvider, + GraphServiceClientOptions defaultAuthenticationOptions) + { + _authorizationHeaderProvider = authorizationHeaderProvider; + _defaultAuthenticationOptions = defaultAuthenticationOptions; + } + + /// + /// Method that performs the authentication, and adds the right headers to the request. + /// + /// + /// + /// + /// + /// + public async Task AuthenticateRequestAsync( + RequestInformation request, + Dictionary? additionalAuthenticationContext = null, + CancellationToken cancellationToken = default) + { + _ = Throws.IfNull(request); + + // Attempts to get the scopes + IEnumerable? 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); + } + } + + /// + /// Transforms the Kiota HTTP Method (enum) into a .NET HttpMethod (static members). + /// + /// Kiota Http method + /// HttpMethod + 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(); + } + } + } +} diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceClientOptions.cs b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceClientOptions.cs new file mode 100644 index 000000000..f7d4f1758 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceClientOptions.cs @@ -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 +{ + /// + /// Options passed-in to call Microsoft Graph. + /// + public class GraphServiceClientOptions : AuthorizationHeaderProviderOptions + { + /// + /// Options used to configure the authentication provider for Microsoft Graph. + /// + public GraphServiceClientOptions() + { + BaseUrl = Constants.GraphBaseUrlV1; + Scopes = new[] { Constants.UserReadScope }; + } + + /// + /// 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") + /// + public IEnumerable Scopes { get; set; } + } +} diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs new file mode 100644 index 000000000..709d4930f --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs @@ -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 +{ + /// + /// Extensions methods on a MicrosoftIdentityAppCallingWebApiAuthenticationBuilder builder + /// to add support to call Microsoft Graph. + /// + public static class GraphServiceCollectionExtensions + { + /// + /// Add support to call Microsoft Graph. From a named option and a configuration section. + /// + /// Builder. + /// The service collection to chain. + public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services) + { + services.AddTokenAcquisition(); + services.AddHttpClient(); + return services.AddMicrosoftGraph(options => { }); + } + + /// + /// Add support to call Microsoft Graph. From a base Graph URL and a default scope. + /// + /// Builder. + /// Configuration section containing the Microsoft graph config. + /// The service collection to chain. + public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services, IConfiguration configurationSection) + { + return services.AddMicrosoftGraph(o => configurationSection.Bind(o)); + } + + /// + /// Add support to call Microsoft Graph. From a base Graph URL and a default scope. + /// + /// Builder. + /// Delegate to configure the graph service options + /// The service collection to chain. + public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services, Action configureMicrosoftGraphOptions) + { + // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests + services.AddOptions().Configure(configureMicrosoftGraphOptions); + + services.AddScoped(serviceProvider => + { + var authorizationHeaderProvider = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + var httpClientFactory = serviceProvider.GetRequiredService(); + 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; + } + } +} diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj b/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj new file mode 100644 index 000000000..84e94eab4 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj @@ -0,0 +1,22 @@ + + + + Microsoft Identity Web, Microsoft Graph v5+ helper + Microsoft Identity Web + + 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. + + {4DF02DF7-D092-4F45-8892-8A1D3E612706} + + + + + + + + + + + diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/Properties/InternalsVisibleTo.cs b/src/Microsoft.Identity.Web.GraphServiceClient/Properties/InternalsVisibleTo.cs new file mode 100644 index 000000000..4d20fccd3 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/Properties/InternalsVisibleTo.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +// Allow this assembly to be serviced when run on desktop CLR +[assembly: InternalsVisibleTo("Microsoft.Identity.Web.Test.Integration, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] +[assembly: InternalsVisibleTo("GraphServiceClientTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] +[assembly: InternalsVisibleTo("Microsoft.Identity.Web.GraphServiceClientBeta, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/Readme.md b/src/Microsoft.Identity.Web.GraphServiceClient/Readme.md new file mode 100644 index 000000000..852f202aa --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/Readme.md @@ -0,0 +1,282 @@ +# Microsoft.Identity.Web.GraphServiceClient + +With the introduction of Microsoft.Identity.Web.GraphServiceClient and Microsoft.Identity.Web.GraphServiceClientBeta +libraries in version Microsoft.Identity.Web 2.12, you now have the choice to use either the legacy +Microsoft.Identity.Web.MicrosoftGraph and Microsoft.Identity.Web.MicrosoftGraphBeta NuGet packages +based on Microsoft Graph SDK 4.x or the new libraries based on Microsoft Graph SDK 5. +By keeping both options available, you can choose to migrate to the latest version of the SDK at your own pace +and with minimal disruption to your existing code. + +By migrating to Microsoft.Identity.Web.GraphServiceClient, you'll benefit from the latest features of the Microsoft Graph SDK, +including a simplified fluent API and the ability to use both Microsoft Graph and Microsoft Graph Beta APIs in the same application. +However, migrating from Microsoft.Identity.Web.MicrosoftGraph 2.x to Microsoft.Identity.Web.GraphServiceClient requires moving some of your code, +as discussed in the [migration guide](#migrate-from-microsoftidentitywebmicrosoftgraph-2x-to-microsoftidentitywebgraphserviceclient). + +## Usage + +1. Reference Microsoft.Identity.Web.GraphServiceClient in your project. + ```shell + dotnet add package Microsoft.Identity.Web.GraphServiceClient + ``` + +1. In the startup method, add Microsoft Graph support to the service collection. + By default, the scopes are set to `User.Read` and the BaseUrl is "https://graph.microsoft.com/v1.0". + You can change them by passing a delegate to the `AddMicrosoftGraph` method (See below). + + Use the following namespace. + ```csharp + using Microsoft.Identity.Web; + ``` + + Add the Microsoft Graph + + ```csharp + services.AddMicrosoftGraph(); + ``` + + or, if you have described Microsoft Graph options in your configuration file: + ```json + "AzureAd": + { + // more here + }, + + "DownstreamApis": + { + "MicrosoftGraph": + { + // Specify BaseUrl if you want to use Microsoft graph in a national cloud. + // See https://learn.microsoft.com/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints + // "BaseUrl": "https://graph.microsoft.com/v1.0", + + // Set RequestAppToken this to "true" if you want to request an application token (to call graph on + // behalf of the application). The scopes will then automatically + // be ['https://graph.microsoft.com/.default']. + // "RequestAppToken": false + + // Set Scopes to request (unless you request an app token). + "Scopes": ["User.Read", "User.ReadBasic.All"] + + // See https://aka.ms/ms-id-web/downstreamApiOptions for all the properties you can set. + } + } + ``` + + The code to add Microsoft Graph based on the configuration is: + + ```csharp + services.AddMicrosoftGraph(); + services.Configure(options => + services.Configuration.GetSection("DownstreamApis:MicrosoftGraph")); + ``` + + or + + ```csharp + services.AddMicrosoftGraph(options => + services.Configuration.GetSection("DownstreamApis:MicrosoftGraph").Bind(options) ); + ``` + +2. Inject the GraphServiceClient from the constructor of controllers. + ```csharp + using Microsoft.Graph; + + public class HomeController : Controller + { + private readonly GraphServiceClient _graphServiceClient; + public HomeController(GraphServiceClient graphServiceClient) + { + _graphServiceClient = graphServiceClient; + } + } + ``` + +3. Use Microsoft Graph SDK to call Microsoft Graph. For example, to get the current user's profile: + ```csharp + var user = await _graphServiceClient.Me.GetAsync(); + ``` + +4. You can override the default options in the GetAsync(), PostAsync() etc.. methods. + For example to get the mail folders of the current user, you'll need to request more scopes ("Mail.Read"). + If your app registered several authentication schemes in ASP.NET Core, you'll also need to specify + which to authentication scheme to apply. + + ```csharp + var mailFolders = await _graphServiceClient.Me.MailFolders.GetAsync(r => + { + r.Options.WithScopes("Mail.Read") + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme); + }); + ``` + + You could also write the same code as follows, which is more verbose, but enables you to set several options at once: + + ```csharp + var mailFolders = await _graphServiceClient.Me.MailFolders.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + // Specify scopes for the request + o.Scopes = new string[] { "Mail.Read" }; + + // Specify the ASP.NET Core authentication scheme if needed (in the case + // of multiple authentication schemes) + o.AcquireTokenOptions.AuthenticationOptionsName = JwtBearerDefaults.AuthenticationScheme; + }); + }); + ``` + + If your app calls the Graph API on behalf of itself, you'll need to request an application token. + You do this by setting WithAppOnly. For instance to get the number of applications in the tenant: + + ```charp + int? appsInTenant = await _graphServiceClient.Applications.Count.GetAsync( + r => r.Options.WithAppOnly() ); + ``` + + which is a shortcut for: + + ```charp + int? appsInTenant = await _graphServiceClient.Applications.Count.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + // Applications require app permissions, hence an app token + o.RequestAppToken = true; + }); + }); + ``` + +## You can now use both Microsoft Graph and Microsoft Graph Beta + +You can now use both Microsoft Graph and Microsoft Graph Beta in the same application: + +1. Reference both Microsoft.Identity.Web.GraphServiceClient and Microsoft.Identity.Web.GraphServiceClientBeta in your project + ```shell + dotnet add package Microsoft.Identity.Web.GraphServiceClient + dotnet add package Microsoft.Identity.Web.GraphServiceClientBeta + ``` + +1. In the startup method, add Microsoft Graph and Graph Beta to the service collection: + + ```csharp + services.AddMicrosoftGraph(); + services.AddMicrosoftGraphBeta(); + ``` + +1. In the controller or wherever you want to use them declare both GraphServiceClient and GraphServiceClientBeta + and inject them in the constructor: + + ```csharp + using GraphServiceClient = Microsoft.Graph.GraphServiceClient; + using GraphBetaServiceClient = Microsoft.Graph.GraphBetaServiceClient; + ``` + + ```csharp + MyController(GraphServiceClient graphServiceClient, GraphBetaServiceClient graphServiceClient) + { + // more here + } + ``` + +## Migrate from Microsoft.Identity.Web.MicrosoftGraph 2.x to Microsoft.Identity.Web.GraphServiceClient + +Microsoft.Identity.Web.GraphServiceClient is based on Microsoft.GraphSDK 5.x, which introduces breaking changes. +The Request() method has disappeared, and the extension methods it enabled in Microsoft.Identity.Web.MicrosoftGraph +are now moved to the GetAsync(), GetPost(), etc methods. + +The Microsoft Graph 4.x code: + +```csharp +var user = await _graphServiceClient.Me.Request().GetAsync(); +``` + +becomes with Microsoft.Graph 5.x: + +```csharp +var user = await _graphServiceClient.Me.GetAsync(); +``` + +The following paragraphs help you migrate from Microsoft.Identity.Web.MicrosoftGraph to Microsoft.Identity.Web.GraphServiceClient. + +### Replace the nuget packages + +1. Reference Microsoft.Identity.Web.GraphServiceClient in your project. + ```shell + dotnet remove package Microsoft.Identity.Web.MicrosoftGraph + dotnet add package Microsoft.Identity.Web.GraphServiceClient + ``` + +### Update the code + +In addition to the changes to the code due to the migration from Microsoft.Graph 4.x to Microsoft.Graph 5.x, you need to change the location of the +modifiers `.WithScopes()`, `.WithAppOnly()`, `WithAuthenticationScheme()` and `.WithAuthenticationOptions()`. + +#### WithScopes() + +In Microsoft.Identity.Web.MicrosoftGraph, you used to use `.WithScopes()` on the request to specify scopes to use to authenticate to Microsoft Graph: +```csharp +var messages = await _graphServiceClient.Users + .Request() + .WithScopes("User.Read.All") + .GetAsync(); +int NumberOfUsers = messages.Count; +``` + +With Microsoft.Identity.Web.GraphServiceClient, you need to call `.WithScopes()` on the options of the builder. + +```csharp +var messages = await _graphServiceClient.Users + .GetAsync(b => b.Options.WithScopes("User.Read.All")); +int NumberOfUsers = messages.Value.Count; +``` + +#### WithAppOnly() + +In Microsoft.Identity.Web.MicrosoftGraph 2.x, you could specify using app permissions (which require an app-only-token) by calling `.WithAppOnly()`. + +```csharp +var messages = await _graphServiceClient.Users + .Request() + .WithAppOnly() + .GetAsync(); +int NumberOfUsers = messages.Count; +``` + +With Microsoft.Identity.Web.GraphServiceClient, you need to call `.WithAppOnly()` on the options of the builder. + +```csharp +var messages = await _graphServiceClient.Users + .GetAsync(b => b.Options.WithAppOnly() )); +int NumberOfUsers = messages.Value.Count; +``` + +Note that this will use, under the hood, the scopes **["https://graph.microsoft.com/.default"]** which means all the pre-authorized scopes. You don't need +to specify these scopes, as this is the only possible when calling a Microsof Graph API requiring app permissions. + +#### WithAuthenticationScheme() in ASP.NET Core applications. + +If you are using Microsoft.Identity.Web.MicrosoftGraph in an ASP.NET Core application, you can specify the authentication scheme +to use by calling `WithAuthenticationScheme()`. + +```csharp +var messages = await _graphServiceClient.Users + .Request() + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme) + .GetAsync(); +int NumberOfUsers = messages.Count; +``` + +With Microsoft.Identity.Web.GraphServiceClient, this becomes: + +```csharp +var messages = await _graphServiceClient.Users + .GetAsync(b => b.Options.WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme) )); +int NumberOfUsers = messages.Value.Count; +``` + +More information about the migration from Microsoft Graph SDK 4.x to 5.x can be found in [Microsoft Graph .NET SDK v5 changelog and upgrade guide](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/upgrade-to-v5.md) + +#### Other authentication options + +You can use .WithAuthenticationOptions() on the builder options. diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/RequestOptionsExtension.cs b/src/Microsoft.Identity.Web.GraphServiceClient/RequestOptionsExtension.cs new file mode 100644 index 000000000..e1205634d --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/RequestOptionsExtension.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Kiota.Abstractions; + +namespace Microsoft.Identity.Web +{ + /// + /// Extension class on the + /// + public static class RequestOptionsExtension + { + /// + /// Specify the authentication options for the request. + /// + /// Options to modify. + /// Authentication request options to set. + /// + public static IList WithAuthenticationOptions(this IList options, GraphAuthenticationOptions optionsValue) + { + GraphAuthenticationOptions? graphAuthenticationOptions = options.OfType().FirstOrDefault(); + if (graphAuthenticationOptions != null) + { + throw new ArgumentException("Can't add GraphAuthenticationOptions twice. Rather use the delegate."); + } + options.Add(optionsValue); + return options; + } + + /// + /// Specify a delegate setting the authentication options for the request. + /// + /// Options to modify. + /// Delegate setting the authentication request. + /// + public static IList WithAuthenticationOptions(this IList options, Action optionsValue) + { + GraphAuthenticationOptions? graphAuthenticationOptions = options.OfType().FirstOrDefault(); + if (graphAuthenticationOptions == null) + { + graphAuthenticationOptions = new GraphAuthenticationOptions(); + options.Add(graphAuthenticationOptions); + } + optionsValue(graphAuthenticationOptions); + return options; + } + + /// + /// Specify the scopes to use to request a token for the request. + /// + /// Options to modify. + /// Microsoft Graph scopes used to authenticate this request. + /// + public static IList WithScopes(this IList options, params string[] scopes) + { + GraphAuthenticationOptions? graphAuthenticationOptions = options.OfType().FirstOrDefault(); + if (graphAuthenticationOptions == null) + { + graphAuthenticationOptions = new GraphAuthenticationOptions(); + options.Add(graphAuthenticationOptions); + } + graphAuthenticationOptions.Scopes = scopes; + return options; + } + + /// + /// Specifies to use app only permissions for Graph. + /// + /// Options to modify. + /// Should the permissions be app only or not. + /// Tenant ID or domain for which we want to make the call.. + /// + public static IList WithAppOnly(this IList options, bool appOnly=true, string? tenant=null) + { + GraphAuthenticationOptions? graphAuthenticationOptions = options.OfType().FirstOrDefault(); + if (graphAuthenticationOptions == null) + { + graphAuthenticationOptions = new GraphAuthenticationOptions(); + options.Add(graphAuthenticationOptions); + } + graphAuthenticationOptions.RequestAppToken = appOnly; + graphAuthenticationOptions.AcquireTokenOptions ??= new(); + graphAuthenticationOptions.AcquireTokenOptions.Tenant = tenant; + return options; + } + +#if NETCOREAPP + /// + /// In ASP.NET Core, specify the authentication scheme used to authenticate this request. + /// + /// Options to modify. + /// ASP.NET Core authentication scheme used to authenticate this request. + /// + public static IList WithAuthenticationScheme(this IList options, string authenticationScheme) + { + GraphAuthenticationOptions? graphAuthenticationOptions = options.OfType().FirstOrDefault(); + if (graphAuthenticationOptions == null) + { + graphAuthenticationOptions = new GraphAuthenticationOptions(); + options.Add(graphAuthenticationOptions); + } + graphAuthenticationOptions.AcquireTokenOptions ??= new(); + graphAuthenticationOptions.AcquireTokenOptions.AuthenticationOptionsName = authenticationScheme; + return options; + } +#endif + } +} diff --git a/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs new file mode 100644 index 000000000..dc1247088 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs @@ -0,0 +1,75 @@ +// 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.Beta; +using Microsoft.Identity.Abstractions; + +namespace Microsoft.Identity.Web +{ + /// + /// Extensions methods on a MicrosoftIdentityAppCallingWebApiAuthenticationBuilder builder + /// to add support to call Microsoft Graph Beta. + /// + public static class GraphBetaServiceCollectionExtensions + { + /// + /// Add support to call Microsoft Graph Beta. From a named option and a configuration section. + /// + /// Builder. + /// The service collection to chain. + public static IServiceCollection AddMicrosoftGraphBeta(this IServiceCollection services) + { + services.AddTokenAcquisition(); + services.AddHttpClient(); + return services.AddMicrosoftGraphBeta(options => { }); + } + + /// + /// Add support to call Microsoft Graph Beta. From a base Graph URL and a default scope. + /// + /// Builder. + /// Configuration section containing the Microsoft graph config. + /// The service collection to chain. + public static IServiceCollection AddMicrosoftGraphBeta(this IServiceCollection services, IConfiguration configurationSection) + { + return services.AddMicrosoftGraphBeta(o => configurationSection.Bind(o)); + } + + /// + /// Add support to call Microsoft Graph Beta. From a base Graph URL and a default scope. + /// + /// Builder. + /// Delegate to configure the graph service options + /// The service collection to chain. + public static IServiceCollection AddMicrosoftGraphBeta(this IServiceCollection services, Action configureMicrosoftGraphOptions) + { + // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests + services.AddOptions().Configure(configureMicrosoftGraphOptions); + + services.AddScoped(serviceProvider => + { + var authorizationHeaderProvider = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var microsoftGraphOptions = options.Value; + if (microsoftGraphOptions.Scopes == null) + { + Throws.ArgumentNullException("scopes", IDWebErrorMessage.CalledApiScopesAreNull); + } + + var httpClient = httpClientFactory.CreateClient("GraphServiceClientBeta"); + + GraphServiceClient betaGraphServiceClient = new(httpClient, + new GraphAuthenticationProvider(authorizationHeaderProvider, microsoftGraphOptions), microsoftGraphOptions.BaseUrl); + return betaGraphServiceClient; + }); + + return services; + } + } +} diff --git a/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj b/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj new file mode 100644 index 000000000..4e1cb0698 --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj @@ -0,0 +1,23 @@ + + + + Microsoft Identity Web, Microsoft Graph v5+ helper + Microsoft Identity Web + + 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. + + {608F0E0B-A52D-4E0F-9B1A-BA9BDA866484} + + + + + + + + + + + + diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Properties/InternalsVisibleTo.cs b/src/Microsoft.Identity.Web.TokenAcquisition/Properties/InternalsVisibleTo.cs index f5d58958e..dbc337c80 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/Properties/InternalsVisibleTo.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/Properties/InternalsVisibleTo.cs @@ -6,6 +6,8 @@ // Allow this assembly to be serviced when run on desktop CLR [assembly: InternalsVisibleTo("Microsoft.Identity.Web, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] [assembly: InternalsVisibleTo("Microsoft.Identity.Web.OWIN, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] +[assembly: InternalsVisibleTo("Microsoft.Identity.Web.GraphServiceClient, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] +[assembly: InternalsVisibleTo("Microsoft.Identity.Web.GraphServiceClientBeta, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] [assembly: InternalsVisibleTo("Microsoft.Identity.Web.MicrosoftGraph, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] [assembly: InternalsVisibleTo("Microsoft.Identity.Web.MicrosoftGraphBeta, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] [assembly: InternalsVisibleTo("Microsoft.Identity.Web.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] diff --git a/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllApps.cshtml.cs b/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllApps.cshtml.cs index abb3d2016..784293575 100644 --- a/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllApps.cshtml.cs +++ b/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllApps.cshtml.cs @@ -28,10 +28,9 @@ public AllAppsModel(ILogger logger, GraphServiceClient graphServiceC public async Task OnGet() { var messages = await _graphServiceClient.Applications - .Request() - .WithAppOnly() - .GetAsync(); - NumberOfApps = messages.Count; + .GetAsync(r => r.Options.WithAppOnly()); + ; + NumberOfApps = messages.Value.Count; } } } diff --git a/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllUsers.cshtml.cs b/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllUsers.cshtml.cs index 8420d0c2a..73599bc60 100644 --- a/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllUsers.cshtml.cs +++ b/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/AllUsers.cshtml.cs @@ -26,10 +26,8 @@ public AllUsersModel(ILogger logger, GraphServiceClient graphService public async Task OnGet() { var messages = await _graphServiceClient.Users - .Request() - .WithScopes("User.Read.All") - .GetAsync(); - NumberOfUsers = messages.Count; + .GetAsync(b => b.Options.WithAppOnly()); + NumberOfUsers = messages.Value.Count; } } } diff --git a/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/Index.cshtml.cs b/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/Index.cshtml.cs index fd8c4e72d..8e50a59c7 100644 --- a/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/Index.cshtml.cs +++ b/tests/DevApps/WebAppCallsMicrosoftGraph/Pages/Index.cshtml.cs @@ -29,11 +29,11 @@ public IndexModel(ILogger logger, GraphServiceClient graphServiceCli public async Task OnGet() { - var user = await _graphServiceClient.Me.Request().GetAsync(); + var user = await _graphServiceClient.Me.GetAsync(r => r.Options.WithScopes("user.read")); try { - using (var photoStream = await _graphServiceClient.Me.Photo.Content.Request().GetAsync()) + using (var photoStream = await _graphServiceClient.Me.Photo.Content.GetAsync()) { byte[] photoByte = ((MemoryStream)photoStream).ToArray(); ViewData["photo"] = Convert.ToBase64String(photoByte); diff --git a/tests/DevApps/WebAppCallsMicrosoftGraph/Startup.cs b/tests/DevApps/WebAppCallsMicrosoftGraph/Startup.cs index 6ee4dda54..bcc7e9736 100644 --- a/tests/DevApps/WebAppCallsMicrosoftGraph/Startup.cs +++ b/tests/DevApps/WebAppCallsMicrosoftGraph/Startup.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Microsoft.Identity.Web.UI; namespace WebAppCallsMicrosoftGraph @@ -36,7 +37,9 @@ public void ConfigureServices(IServiceCollection services) services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection(configSection)) - .EnableTokenAcquisitionToCallDownstreamApi() + .EnableTokenAcquisitionToCallDownstreamApi(); + + services .AddMicrosoftGraph(Configuration.GetSection("GraphBeta")) .AddDownstreamApi("GraphBeta", Configuration.GetSection("GraphBeta")) .AddInMemoryTokenCaches(); diff --git a/tests/DevApps/WebAppCallsMicrosoftGraph/WebAppCallsMicrosoftGraph.csproj b/tests/DevApps/WebAppCallsMicrosoftGraph/WebAppCallsMicrosoftGraph.csproj index 6393f06b4..460db32e8 100644 --- a/tests/DevApps/WebAppCallsMicrosoftGraph/WebAppCallsMicrosoftGraph.csproj +++ b/tests/DevApps/WebAppCallsMicrosoftGraph/WebAppCallsMicrosoftGraph.csproj @@ -4,7 +4,7 @@ - + diff --git a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config index 8a3c3f1e9..52a3a77f0 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config @@ -98,7 +98,7 @@ - + diff --git a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config index d7e067c46..882eff566 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config @@ -99,7 +99,7 @@ - + diff --git a/tests/IntegrationTests/GraphServiceClientTests/GraphServiceClientTests.cs b/tests/IntegrationTests/GraphServiceClientTests/GraphServiceClientTests.cs new file mode 100644 index 000000000..44b65e461 --- /dev/null +++ b/tests/IntegrationTests/GraphServiceClientTests/GraphServiceClientTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.Graph.Models; +using Microsoft.Identity.Abstractions; + +namespace Microsoft.Identity.Web.Test.Integration +{ + + /// + /// This is a compilation test only. It is not meant to be run. + /// + public class GraphServiceClientTests + { +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +#pragma warning disable CS0649 // Field 'GraphServiceClientTests._authorizationHeaderProvider' is never assigned to, and will always have its default value null + readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; + readonly GraphServiceClientOptions _defaultAuthenticationOptions; +#pragma warning restore CS0649 // Field 'GraphServiceClientTests._authorizationHeaderProvider' is never assigned to, and will always have its default value null +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +#pragma warning disable IDE0051 // Remove unused private members + async Task Test() +#pragma warning restore IDE0051 // Remove unused private members + { + GraphServiceClient graphServiceClient = new(new GraphAuthenticationProvider(_authorizationHeaderProvider, new GraphServiceClientOptions())); + + User? me = await graphServiceClient.Me.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + o.Scopes = new string[] { "user.read" }; + o.RequestAppToken = true; + o.ProtocolScheme = "Pop"; + o.AcquireTokenOptions.Claims = "claims"; + o.AcquireTokenOptions.PopPublicKey = ""; + o.AcquireTokenOptions.CorrelationId = Guid.NewGuid(); + o.AcquireTokenOptions.UserFlow = "susi"; + o.AcquireTokenOptions.AuthenticationOptionsName = "JwtBearer"; + o.AcquireTokenOptions.Tenant = "TenantId"; + }); + } + ); + + MailFolderCollectionResponse? mailFolders = await graphServiceClient.Me.MailFolders.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + // Specify scopes for the request + o.Scopes = new string[] { "Mail.Read" }; + + // Specify the ASP.NET Core authentication scheme if needed (in the case + // of multiple authentication schemes) + // o.AuthenticationOptionsName = JwtBearerDefaults.AuthenticationScheme; + }); + }); + + int? appsInTenant = await graphServiceClient.Applications.Count.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + // It's an app permission. Requires an app token + o.RequestAppToken = true; + }); + }); + + } + } +} diff --git a/tests/IntegrationTests/GraphServiceClientTests/GraphServiceClientTests.csproj b/tests/IntegrationTests/GraphServiceClientTests/GraphServiceClientTests.csproj new file mode 100644 index 000000000..a7f312181 --- /dev/null +++ b/tests/IntegrationTests/GraphServiceClientTests/GraphServiceClientTests.csproj @@ -0,0 +1,27 @@ + + + + net7.0 + ../../../build/MSAL.snk + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/IntegrationTests/GraphServiceClientTests/Usings.cs b/tests/IntegrationTests/GraphServiceClientTests/Usings.cs new file mode 100644 index 000000000..8190c642f --- /dev/null +++ b/tests/IntegrationTests/GraphServiceClientTests/Usings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +global using Xunit; \ No newline at end of file diff --git a/tests/IntegrationTests/TokenAcquirerTests/TokenAcquirer.cs b/tests/IntegrationTests/TokenAcquirerTests/TokenAcquirer.cs index eaf85f419..86e7e7c26 100644 --- a/tests/IntegrationTests/TokenAcquirerTests/TokenAcquirer.cs +++ b/tests/IntegrationTests/TokenAcquirerTests/TokenAcquirer.cs @@ -196,7 +196,7 @@ private static RsaSecurityKey CreateRsaSecurityKey() // the reason for creating the RsaSecurityKey from RSAParameters is so that a SignatureProvider created with this key // will own the RSA object and dispose it. If we pass a RSA object, the SignatureProvider does not own the object, the RSA object will not be disposed. RSAParameters rsaParameters = rsa.ExportParameters(true); - RsaSecurityKey rsaSecuirtyKey = new RsaSecurityKey(rsaParameters) { KeyId = CreateRsaKeyId(rsaParameters) }; + RsaSecurityKey rsaSecuirtyKey = new(rsaParameters) { KeyId = CreateRsaKeyId(rsaParameters) }; rsa.Dispose(); return rsaSecuirtyKey; } @@ -210,7 +210,9 @@ private static string CreateRsaKeyId(RSAParameters rsaParameters) Array.Copy(rsaParameters.Exponent, 0, kidBytes, 0, rsaParameters.Exponent.Length); Array.Copy(rsaParameters.Modulus, 0, kidBytes, rsaParameters.Exponent.Length, rsaParameters.Modulus.Length); using (var sha2 = SHA256.Create()) + { return Base64UrlEncoder.Encode(sha2.ComputeHash(kidBytes)); + } } private string? ComputePublicKeyString(X509Certificate2? certificate)