Skip to content
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
13 changes: 9 additions & 4 deletions aspnetcore/blazor/security/webassembly/graph-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ After adding the Microsoft Graph API scopes in the ME-ID area of the Azure porta

```json
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"BaseUrl": "https://graph.microsoft.com/{VERSION}",
"Scopes": [
"user.read"
]
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the MS Graph API (for example: `v1.0`).

:::moniker range=">= aspnetcore-8.0"

Add the following `GraphClientExtensions` class to the standalone app. The scopes are provided to the <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.AccessTokenRequestOptions.Scopes> property of the <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.AccessTokenRequestOptions> in the `AuthenticateRequestAsync` method.
Expand Down Expand Up @@ -268,7 +270,6 @@ public class CustomAccountFactory
userIdentity.AddClaim(new Claim("officelocation",
user.OfficeLocation ?? "Not set"));
}

}
catch (AccessTokenNotAvailableException exception)
{
Expand Down Expand Up @@ -387,13 +388,15 @@ After adding the Microsoft Graph API scopes in the ME-ID area of the Azure porta

```json
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"BaseUrl": "https://graph.microsoft.com/{VERSION}",
"Scopes": [
"user.read"
]
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the MS Graph API (for example: `v1.0`).

:::moniker range=">= aspnetcore-8.0"

Add the following `GraphClientExtensions` class to the standalone app. The scopes are provided to the <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.AccessTokenRequestOptions.Scopes> property of the <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.AccessTokenRequestOptions> in the `AuthenticateRequestAsync` method. The <xref:Microsoft.Graph.IHttpProvider.OverallTimeout?displayProperty=nameWithType> is extended from the default value of 100 seconds to 300 seconds to give the `HttpClient` more time to receive a response from Microsoft Graph.
Expand Down Expand Up @@ -730,13 +733,15 @@ After adding the Microsoft Graph API scopes in the ME-ID area of the Azure porta

```json
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"BaseUrl": "https://graph.microsoft.com/{VERSION}",
"Scopes": [
"user.read"
]
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the MS Graph API (for example: `v1.0`).

Create the following `GraphAuthorizationMessageHandler` class and project configuration in the `Program` file for working with Graph API. The base URL and scopes are provided to the handler from configuration.

`GraphAuthorizationMessageHandler.cs`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ms.author: riande
ms.custom: "devx-track-csharp, mvc"
ms.date: 12/16/2022
uid: blazor/security/webassembly/meid-groups-roles
zone_pivot_groups: blazor-groups-and-roles
---
# Microsoft Entra (ME-ID) groups, Administrator Roles, and App Roles

Expand Down Expand Up @@ -149,6 +150,116 @@ Add the following custom user account factory to the **CLIENT** app. The custom

`CustomAccountFactory.cs`:

:::zone pivot="graph-sdk-5"

The following example assumes that the project's app settings file includes an entry for the base URL:

```json
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/{VERSION}",
...
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the MS Graph API (for example: `v1.0`).

```csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;

public class CustomAccountFactory
: AccountClaimsPrincipalFactory<CustomUserAccount>
{
private readonly ILogger<CustomAccountFactory> logger;
private readonly IServiceProvider serviceProvider;
private readonly string? baseUrl;

public CustomAccountFactory(IAccessTokenProviderAccessor accessor,
IServiceProvider serviceProvider,
ILogger<CustomAccountFactory> logger,
IConfiguration config)
: base(accessor)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
baseUrl = config.GetSection("MicrosoftGraph")["BaseUrl"];
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
CustomUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);

if (initialUser.Identity is not null &&
initialUser.Identity.IsAuthenticated)
{
var userIdentity = initialUser.Identity as ClaimsIdentity;

if (userIdentity is not null && !string.IsNullOrEmpty(baseUrl))
{
account?.Roles?.ForEach((role) =>
{
userIdentity.AddClaim(new Claim("appRole", role));
});

account?.Wids?.ForEach((wid) =>
{
userIdentity.AddClaim(new Claim("directoryRole", wid));
});

try
{
var client = new GraphServiceClient(
new HttpClient(),
serviceProvider
.GetRequiredService<IAuthenticationProvider>(),
baseUrl);

var user = await client.Me.GetAsync();

if (user is not null)
{
userIdentity.AddClaim(new Claim("mobilephone",
user.MobilePhone ?? "(000) 000-0000"));
userIdentity.AddClaim(new Claim("officelocation",
user.OfficeLocation ?? "Not set"));
}

var requestMemberOf = client.Users[account?.Oid].MemberOf;
var memberships = await requestMemberOf.Request().GetAsync();

if (memberships is not null)
{
foreach (var entry in memberships)
{
if (entry.ODataType == "#microsoft.graph.group")
{
userIdentity.AddClaim(
new Claim("directoryGroup", entry.Id));
}
}
}
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

return initialUser;
}
}
```

:::zone-end

:::zone pivot="graph-sdk-4"

```csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
Expand Down Expand Up @@ -235,6 +346,8 @@ public class CustomAccountFactory
}
```

:::zone-end

The preceding code doesn't include transitive memberships. If the app requires direct and transitive group membership claims, replace the `MemberOf` property (`IUserMemberOfCollectionWithReferencesRequestBuilder`) with `TransitiveMemberOf` (`IUserTransitiveMemberOfCollectionWithReferencesRequestBuilder`).

The preceding code ignores group membership claims (`groups`) that are ME-ID Administrator Roles (`#microsoft.graph.directoryRole` type) because the GUID values returned by the Microsoft identity platform are ME-ID Administrator Role **entity IDs** and not [**Role Template IDs**](/azure/active-directory/roles/permissions-reference#role-template-ids). Entity IDs aren't stable across tenants in Microsoft identity platform and shouldn't be used to create authorization policies for users in apps. Always use **Role Template IDs** for ME-ID Administrator Roles **provided by `wids` claims**.
Expand Down
8 changes: 8 additions & 0 deletions aspnetcore/zone-pivot-groups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,11 @@ groups:
title: Server
- id: webassembly
title: Blazor WebAssembly
- id: blazor-groups-and-roles
title: Approach
prompt: Choose the Microsoft Graph SDK version
pivots:
- id: graph-sdk-4
title: Graph SDK v4
- id: graph-sdk-5
title: Graph SDK v5