services | platforms | author | level | client | service | endpoint |
---|---|---|---|---|---|---|
active-directory |
dotnet |
jmprieur |
200 |
ASP.NET Core 2.x Web App |
Microsoft Graph |
Microsoft identity platform |
Using the Microsoft identity platform to call the Microsoft Graph API from an An ASP.NET Core 2.x Web App, on behalf of a user signing-in using their work and school or Microsoft personal account
Starting from a .NET Core MVC Web app that uses OpenID Connect to sign in users, this phase of the tutorial shows how to call Microsoft Graph /me endpoint on behalf of the signed-in user. It leverages the ASP.NET Core OpenID Connect middleware and Microsoft Authentication Library for .NET (MSAL.NET). Their complexities where encapsulated into the Microsoft.Identity.Web
reusable library project part of this tutorial. Once again the notion of ASP.NET services injected by dependency injection is heavily used.
To run this sample:
Pre-requisites:
go through the previous phase of the tutorial showing how the WebApp signs-in users with Microsoft Identity (OIDC) / with work and school or personal accounts. This page shows the incremental change required to call the Microsoft Graph API on behalf of a user that has successfully signed in to the web app.
- Developers who wish to gain good familiarity of programming for Microsoft Graph are advised to go through the An introduction to Microsoft Graph for developers recorded session.
You first need to register your app as described in the first tutorial
Then follow the following extra set of steps:
- In the app's registration screen, click on the Certificates & secrets blade in the left to open the page where we can generate secrets and upload certificates.
- In the Client secrets section, click on New client secret:
- Type a key description (for instance
app secret
), - Select one of the available key durations (In 1 year, In 2 years, or Never Expires) as per your security concerns.
- The generated key value will be displayed when you click the Add button. Copy the generated value for use in the steps later.
- You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Azure portal before navigating to any other screen or blade.
- Type a key description (for instance
- In the app's registration screen, click on the API permissions blade in the left to open the page where we add access to the Apis that your application needs.
- Click the Add permissions button and then,
- Ensure that the Microsoft APIs tab is selected.
- In the Commonly used Microsoft APIs section, click on Microsoft Graph
- In the Delegated permissions section, select the User.Read in the list. Use the search box if necessary.
- Click on the Add permissions button in the bottom.
If you have not already, clone this sample from your shell or command line:
git clone https://github.com/Azure-Samples/microsoft-identity-platform-aspnetcore-webapp-tutorial webapp
cd webapp
Go to the "2-WebApp-graph-user\2-1-Call-MSGraph"
folder
cd "2-WebApp-graph-user\2-1-Call-MSGraph"
Open the project in your IDE (like Visual Studio) to configure the code.
In the steps below, "ClientID" is the same as "Application ID" or "AppId".
- Open the
appsettings.json
file - Find the app key
ClientId
and replace the existing value with the application ID (clientId) of theWebApp-OpenIDConnect-DotNet-code-v2
application copied from the Azure portal. - Find the app key
TenantId
and replace bycommon
, as here you chose to sign-in users with their work or school or personal account. In case you want to sign-in different audiences, refer back to the first phase of the tutorial. - Find the app key
Domain
and replace the existing value with your Azure AD tenant name. - Find the app key
ClientSecret
and replace the existing value with the key you saved during the creation of theWebApp-OpenIDConnect-DotNet-code-v2
app, in the Azure portal.
-
In case you want to deploy your app in Sovereign or national clouds, ensure the
GraphApiUrl
option matches the one you want. By default this is Microsoft Graph in the Azure public cloud"GraphApiUrl": "https://graph.microsoft.com/v1.0"
-
Build the solution and run it.
-
Open your web browser and make a request to the app. The app immediately attempts to authenticate you via the Microsoft identity platform endpoint. Sign in with your personal account or with a work or school account.
-
Go to the Profile page, you should now see all kind of information about yourself as well as your picture (a call was made to the Microsoft Graph /me endpoint)
Starting from the previous phase of the tutorial, the code was incrementally updated with the following steps:
After the following lines in the ConfigureServices(IServiceCollection services) method, replace services.AddMicrosoftIdentityPlatformAuthentication(Configuration);
, by the following lines:
public void ConfigureServices(IServiceCollection services)
{
. . .
// Token acquisition service based on MSAL.NET
// and chosen token cache implementation
services.AddMicrosoftIdentityPlatformAuthentication(Configuration)
.AddMsal(Configuration, new string[] { Constants.ScopeUserRead })
.AddInMemoryTokenCache();
The two new lines of code:
-
enable MSAL.NET to hook-up to the OpenID Connect events and redeem the authorization code obtained by the ASP.NET Core middleware and after obtaining a token, saves it into the token cache, for use by the Controllers.
-
Decide which token cache implementation to use. In this part of the phase, we'll use a simple in memory token cache, but next steps will show you other implementations you can benefit from, including distributed token caches based on a SQL database, or a Redis cache.
Note that you can replace the in memory token cache serialization by a session token cache (stored in a session cookie). To do this replacement, change the following in Startup.cs:
- replace
using Microsoft.Identity.Web.TokenCacheProviders.InMemory
byusing Microsoft.Identity.Web.TokenCacheProviders.Session
- Replace
.AddInMemoryTokenCaches()
by.AddSessionTokenCaches()
addapp.UseSession();
in theConfigure(IApplicationBuilder app, IHostingEnvironment env)
method, for instance afterapp.UseCookiePolicy();
You can also use a distributed token cache, and choose the serialization implementation. For this, in Startup.cs:
-
replace
using Microsoft.Identity.Web.TokenCacheProviders.InMemory
byusing Microsoft.Identity.Web.TokenCacheProviders.Distributed
-
Replace
.AddInMemoryTokenCaches()
by.AddDistributedTokenCaches()
-
Then choose the distributed cache implementation. For details, see https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2#distributed-memory-cache
// use a distributed Token Cache by adding .AddDistributedTokenCaches(); // and then choose your implementation. // For instance the distributed in memory cache (not cleaned when you stop the app) services.AddDistributedMemoryCache() // Or a Redis cache services.AddStackExchangeRedisCache(options => { options.Configuration = "localhost"; options.InstanceName = "SampleInstance"; }); // Or even a SQL Server token cache services.AddDistributedSqlServerCache(options => { options.ConnectionString =_config["DistCache_ConnectionString"]; options.SchemaName = "dbo"; options.TableName = "TestCache"; });
- replace
Add Microsoft.Graph
package, to use Microsoft Graph SDK.
Add the Services\*.cs
files. The GraphServiceClientFactory.cs
returns a GraphServiceClient
with an authentication provider, used for Microsoft Graph SDK. Given an access token for Microsoft Graph, it's capable of making a request to Graph services sending that access token in the header.
Still in the Startup.cs
file, add the following lines just after the following. This lines ensures that the GraphAPIService benefits from the optimized HttpClient
management by ASP.NET Core.
// Add Graph
services.AddGraphService(Configuration);
In the Controllers\HomeController.cs
file:
- Add a constructor to HomeController, making the ITokenAcquisition service available (used by the ASP.NET dependency injection mechanism)
readonly ITokenAcquisition tokenAcquisition;
readonly WebOptions webOptions;
public HomeController(ITokenAcquisition tokenAcquisition, IOptions<WebOptions> webOptionValue)
{
this.tokenAcquisition = tokenAcquisition;
this.webOptions = webOptionValue.Value;
}
- Add a
Profile()
action so that it calls the Microsoft Graph me endpoint. In case a token cannot be acquired, a challenge is attempted to re-sign-in the user, and have them consent to the requested scopes. This is expressed declaratively by theAuthorizeForScopes
attribute. This attribute is part of theMicrosoft.Identity.Web
project and automatically manages incremental consent.
[AuthorizeForScopes(Scopes = new[] { Constants.ScopeUserRead })]
public async Task<IActionResult> Profile()
{
// Initialize the GraphServiceClient.
Graph::GraphServiceClient graphClient = GetGraphServiceClient(new[] { Constants.ScopeUserRead });
var me = await graphClient.Me.Request().GetAsync();
ViewData["Me"] = me;
try
{
// Get user photo
using (var photoStream = await graphClient.Me.Photo.Content.Request().GetAsync())
{
byte[] photoByte = ((MemoryStream)photoStream).ToArray();
ViewData["Photo"] = Convert.ToBase64String(photoByte);
}
}
catch (System.Exception)
{
ViewData["Photo"] = null;
}
return View();
}
private Graph::GraphServiceClient GetGraphServiceClient(string[] scopes)
{
return GraphServiceClientFactory.GetAuthenticatedGraphClient(async () =>
{
string result = await tokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(scopes);
return result;
}, webOptions.GraphApiUrl);
}
Add a new view Views\Home\Profile.cshtml
and insert the following code, which creates an
HTML table displaying the properties of the me object as returned by Microsoft Graph.
@using Newtonsoft.Json.Linq
@{
ViewData["Title"] = "Profile";
}
<h2>@ViewData["Title"]</h2>
<h3>@ViewData["Message"]</h3>
<table class="table table-striped table-condensed" style="font-family: monospace">
<tr>
<th>Property</th>
<th>Value</th>
</tr>
<tr>
<td>photo</td>
<td>
@{
if (ViewData["photo"] != null)
{
<img style="margin: 5px 0; width: 150px" src="data:image/jpeg;base64, @ViewData["photo"]" />
}
else
{
<h3>NO PHOTO</h3>
<p>Check user profile in Azure Active Directory to add a photo.</p>
}
}
</td>
</tr>
@{
var me = ViewData["me"] as Microsoft.Graph.User;
var properties = me.GetType().GetProperties();
foreach (var child in properties)
{
object value = child.GetValue(me);
string stringRepresentation;
if (!(value is string) && value is IEnumerable<string>)
{
stringRepresentation = "["
+ string.Join(", ", (value as IEnumerable<string>).OfType<object>().Select(c => c.ToString()))
+ "]";
}
else
{
stringRepresentation = value?.ToString();
}
<tr>
<td> @child.Name </td>
<td> @stringRepresentation </td>
</tr>
}
}
</table>
- Learn how to enable distributed caches in token cache serialization
- Learn how the same principle you've just learnt can be used to call:
- several Microsoft APIs, which will enable you to learn how incremental consent and conditional access is managed in your Web App
- 3rd party, or even your own Web API, which will enable you to learn about custom scopes
- Learn how Microsoft.Identity.Web works, in particular hooks-up to the ASP.NET Core ODIC events
- Use HttpClientFactory to implement resilient HTTP requests used by the Graph custom service