Skip to content

2-4-Sovereign-Call-MSGraph updated to latest template #417

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,52 +1,71 @@
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using WebApp_OpenIDConnect_DotNet.Models;
using WebApp_OpenIDConnect_DotNet.Services.GraphOperations;
using Constants = WebApp_OpenIDConnect_DotNet.Infrastructure.Constants;

namespace WebApp_OpenIDConnect_DotNet.Controllers
{
[Authorize]
public class HomeController : Controller
{
readonly ITokenAcquisition tokenAcquisition;
private readonly IGraphApiOperations graphApiOperations;

public HomeController(ITokenAcquisition tokenAcquisition,
IGraphApiOperations graphApiOperations)
{
this.tokenAcquisition = tokenAcquisition;
this.graphApiOperations = graphApiOperations;
}

public IActionResult Index()
{
return View();
}

[AuthorizeForScopes(Scopes = new[] { Constants.ScopeUserRead})]
public async Task<IActionResult> Profile()
{
var accessToken =
await tokenAcquisition.GetAccessTokenForUserAsync(new[] {Constants.ScopeUserRead});

var me = await graphApiOperations.GetUserInformation(accessToken);
var photo = await graphApiOperations.GetPhotoAsBase64Async(accessToken);

ViewData["Me"] = me;
ViewData["Photo"] = photo;

return View();
}

[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}
}
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Graph;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using WebApp_OpenIDConnect_DotNet.Models;
using System.IO;

namespace WebApp_OpenIDConnect_DotNet.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

private readonly GraphServiceClient _graphServiceClient;

public HomeController(ILogger<HomeController> logger,
GraphServiceClient graphServiceClient)
{
_logger = logger;
_graphServiceClient = graphServiceClient;
}

[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public async Task<IActionResult> Index()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = user.DisplayName;

return View();
}

[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public async Task<IActionResult> Profile()
{
var me = await _graphServiceClient.Me.Request().GetAsync();
ViewData["Me"] = me;

try
{
// Get user photo
using (var photoStream = await _graphServiceClient.Me.Photo.Content.Request().GetAsync())
{
byte[] photoByte = ((MemoryStream)photoStream).ToArray();
ViewData["Photo"] = Convert.ToBase64String(photoByte);
}
}
catch (System.Exception)
{
ViewData["Photo"] = null;
}

return View();
}
public IActionResult Privacy()
{
return View();
}

[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

This file was deleted.

152 changes: 81 additions & 71 deletions 2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ Go to the `"2-WebApp-graph-user\2-4-Sovereign-Call-MSGraph"` folder
- the `TenantId` by `organizations`, as here you chose to sign-in users with their work or school account. In case you want to sign-in different audiences, refer back to the first phase of the tutorial
- and the `ClientSecret` by the client secret you generated in Step 1.

- The `GraphApiUrl` for US Government cloud is
- The `DownstreamApi:BaseUrl` for US Government cloud is

```JSon
"GraphApiUrl": "https://graph.microsoft.us"
```
```JSon
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.us/beta"
}
```
In case you want to deploy your app in other sovereign or national clouds, go to [Microsoft Graph service root endpoints](https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints).

### Step 3: Run the sample
Expand All @@ -92,42 +94,35 @@ Starting from the [previous phase of the tutorial](../../1-WebApp-OIDC), the cod
After the following lines in the ConfigureServices(IServiceCollection services) method, replace `services.AddAzureAdV2Authentication(Configuration);`, by the following lines:

```CSharp
public void ConfigureServices(IServiceCollection services)
public void ConfigureServices(IServiceCollection services)
{
. . .
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration)

// Token acquisition service based on MSAL.NET
// and chosen token cache implementation
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { Constants.ScopeUserRead })
.AddInMemoryTokenCaches();
services.AddOptions();

string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');

// Token acquisition service based on MSAL.NET
// and chosen token cache implementation

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
```

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.

### Add additional files to call Microsoft Graph

Add the `Services\Microsoft-Graph-Rest\*.cs` files. This is an implementation of a custom service, which encapsulates the call to the Microsoft Graph /me endpoint. Given an access token for Microsoft Graph, it's capable of getting the user information and the photo of the user.

```CSharp
public interface IGraphApiOperations
{
Task<dynamic> GetUserInformation(string accessToken);
Task<string> GetPhotoAsBase64Async(string accessToken);
}
```

### Update the `Startup.cs` file to enable the Microsoft Graph custom service

Still in the `Startup.cs` file, add the following lines just after the following. This line ensures that the GraphAPIService benefits from the optimized `HttpClient` management by ASP.NET Core.

```CSharp
// Add Graph
services.AddGraphService(Configuration);
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
```

### Change the controller code to acquire a token and call Microsoft Graph
Expand All @@ -136,26 +131,28 @@ In the `Controllers\HomeController.cs`file:

1. Add a constructor to HomeController, making the ITokenAcquisition service available (used by the ASP.NET dependency injection mechanism)

```CSharp
public HomeController(ITokenAcquisition tokenAcquisition, IGraphApiOperations graphApiOperations)
{
this.tokenAcquisition = tokenAcquisition;
this.graphApiOperations = graphApiOperations;
```CSharp
private readonly ILogger<HomeController> _logger;

private readonly GraphServiceClient _graphServiceClient;

}
private ITokenAcquisition tokenAcquisition;
private readonly IGraphApiOperations graphApiOperations;
```
public HomeController(ILogger<HomeController> logger,
GraphServiceClient graphServiceClient)
{
_logger = logger;
_graphServiceClient = graphServiceClient;
}
```

1. 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 the `AuthorizeForScopes`attribute. This attribute is part of the `Microsoft.Identity.Web` project and automatically manages incremental consent.

```CSharp
[AuthorizeForScopes(Scopes = new[] {Constants.ScopeUserRead})]
public async Task<IActionResult> Profile()
{
```CSharp
[AuthorizeForScopes(Scopes = new[] {Constants.ScopeUserRead})]
public async Task<IActionResult> Profile()
{
var accessToken =
await tokenAcquisition.GetAccessTokenOnBehalfOfUser(HttpContext,
new[] {Constants.ScopeUserRead});
new[] {Constants.ScopeUserRead});

var me = await graphApiOperations.GetUserInformation(accessToken);
var photo = await graphApiOperations.GetPhotoAsBase64Async(accessToken);
Expand All @@ -164,8 +161,8 @@ In the `Controllers\HomeController.cs`file:
ViewData["Photo"] = photo;

return View();
}
```
}
```

### Add a Profile view to display the *me* object

Expand All @@ -181,37 +178,50 @@ HTML table displaying the properties of the *me* object as returned by Microsoft
<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 JObject;
var children = me.Properties();
foreach (var child in children)
{
<tr>
<td>@child.Name</td>
<td>@child.Value</td>
</tr>
<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>
```

Expand Down

This file was deleted.

Loading