https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/microsoft-entra-id-groups-and-roles?view=aspnetcore-7.0

https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/graph-api?pivots=graph-sdk-5&view=aspnetcore-7.0

https://learn.microsoft.com/en-us/graph/sdks/sdks-overview

Scopes

To permit Microsoft Graph API calls for user profile, role assignment, and group membership data:

  • CLIENT app is configured with the User.Read scope (https://graph.microsoft.com/User.Read) in the Azure portal.
  • SERVER app is configured with the GroupMember.Read.All scope (https://graph.microsoft.com/GroupMember.Read.All) in the Azure portal.

The preceding scopes are required in addition to the scopes required in ME-ID deployment scenarios described by the topics listed earlier (Standalone with Microsoft AccountsStandalone with ME-ID, and Hosted with ME-ID).

For more information, see the Microsoft Graph permissions reference.

 Note

The words “permission” and “scope” are used interchangeably in the Azure portal and in various Microsoft and external documentation sets. This article uses the word “scope” throughout for the permissions assigned to an app in the Azure portal.

Group Membership Claims attribute

In the app’s manifest in the Azure portal for CLIENT and SERVER apps, set the groupMembershipClaims attribute to All. A value of All results in ME-ID sending all of the security groups, distribution groups, and roles of the signed-in user in the well-known IDs claim (wids):

  1. Open the app’s Azure portal registration.
  2. Select Manage > Manifest in the sidebar.
  3. Find the groupMembershipClaims attribute.
  4. Set the value to All ("groupMembershipClaims": "All").
  5. Select the Save button if you made changes.

Blazor WebAssembly Hosted App Setup

This article explains how to use Microsoft Graph API in Blazor WebAssembly apps, which is a RESTful web API that enables apps to access Microsoft Cloud service resources.

Two approaches are available for directly interacting with Microsoft Graph in Blazor apps:

  • Graph SDK: The Microsoft Graph SDKs are designed to simplify building high-quality, efficient, and resilient applications that access Microsoft Graph. Select the Graph SDK button at the top of this article to adopt this approach.
  • Named HttpClient with Graph API: A named HttpClient can issue web API requests to directly to Graph API. Select the Named HttpClient with Graph API button at the top of this article to adopt this approach.

The guidance in this article isn’t meant to replace the primary Microsoft Graph documentation and additional Azure security guidance in other Microsoft documentation sets. Assess the security guidance in the Additional resources section of this article before implementing Microsoft Graph in a production environment. Follow all of Microsoft’s best practices to limit the attack surface area of your apps.

 Important

The scenarios described in this article apply to using Microsoft Entra (ME-ID) as the identity provider, not AAD B2C. Using Microsoft Graph with a client-side Blazor WebAssembly app and the AAD B2C identity provider isn’t supported at this time.

Using a hosted Blazor WebAssembly app is supported, where the Server app uses the Graph SDK/API to provide Graph data to the Client app via web API. For more information, see the Hosted Blazor WebAssembly solutions section of this article.

The examples in this article take advantage of recent .NET features released with ASP.NET Core 6.0 or later. When using the examples in ASP.NET Core 5.0 or earlier, minor modifications are required. However, the text and code examples that pertain to interacting with Microsoft Graph are the same for all versions of ASP.NET Core.

The following guidance applies to Microsoft Graph v5.

The Microsoft Graph SDK for use in Blazor apps is called the Microsoft Graph .NET Client Library.

The Graph SDK examples require the following package references in the standalone Blazor WebAssembly app or the Client app of a hosted Blazor WebAssembly solution:

 Note

For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.

After adding the Microsoft Graph API scopes in the ME-ID area of the Azure portal, add the following app settings configuration to the wwwroot/appsettings.json file, which includes the Graph base URL with Graph version and scopes. In the following example, the User.Read scope is specified for the examples in later sections of this article.JSONCopy

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

Add the following GraphClientExtensions class to the standalone app or Client app of a hosted Blazor WebAssembly solution. The scopes are provided to the Scopes property of the AccessTokenRequestOptions in the AuthenticateRequestAsync method.

When an access token isn’t obtained, the following code doesn’t set a Bearer authorization header for Graph requests.

GraphClientExtensions.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
using IAccessTokenProvider = 
    Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
            this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes.IsNullOrEmpty())
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                new HttpClient(),
                sp.GetRequiredService<IAuthenticationProvider>(),
                baseUrl);
        });

        return services;
    }

    private class GraphAuthenticationProvider : IAuthenticationProvider
    {
        private readonly IConfiguration config;

        public GraphAuthenticationProvider(IAccessTokenProvider tokenProvider,
            IConfiguration config)
        {
            TokenProvider = tokenProvider;
            this.config = config;
        }

        public IAccessTokenProvider TokenProvider { get; }

        public async Task AuthenticateRequestAsync(RequestInformation request, 
            Dictionary<string, object>? additionalAuthenticationContext = null, 
            CancellationToken cancellationToken = default)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                {
                    Scopes = 
                        config.GetSection("MicrosoftGraph:Scopes").Get<string[]>()
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Add("Authorization", 
                    $"{CoreConstants.Headers.Bearer} {token.Value}");
            }
        }
    }
}

In the Program file, add the Graph client services and configuration with the AddGraphClient extension method:C#Copy

var baseUrl = builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"];
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>();

builder.Services.AddGraphClient(baseUrl, scopes);

Call Graph API from a component using the Graph SDK

The following GraphExample component uses an injected GraphServiceClient to obtain the user’s ME-ID profile data and display their mobile phone number. For any test user that you create in ME-ID, make sure that you give the user’s ME-ID profile a mobile phone number in the Azure portal.

GraphExample.razor

@page "/graph-example"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<h1>Microsoft Graph Component Example</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.Models.User? user;

    protected override async Task OnInitializedAsync()
    {
        user = await Client.Me.GetAsync();
    }
}

When testing with the Graph SDK locally, we recommend using a new in-private/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID.

Customize user claims using the Graph SDK

In the following example, the app creates mobile phone number and office location claims for a user from their ME-ID user profile’s data. The app must have the User.Read Graph API scope configured in ME-ID. Any test users for this scenario must have a mobile phone number and office location in their ME-ID profile, which can be added via the Azure portal.

In the following custom user account factory:

  • An ILogger (logger) is included for convenience in case you wish to log information or errors in the CreateUserAsync method.
  • In the event that an AccessTokenNotAvailableException is thrown, the user is redirected to the identity provider to sign into their account. Additional or different actions can be taken when requesting an access token fails. For example, the app can log the AccessTokenNotAvailableException and create a support ticket for further investigation.
  • The framework’s RemoteUserAccount represents the user’s account. If the app requires a custom user account class that extends RemoteUserAccount, swap your custom user account class for RemoteUserAccount in the following code.

CustomAccountFactory.cs:

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<RemoteUserAccount>
{
    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(
        RemoteUserAccount 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))
            {
                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"));
                    }

                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

Configure the MSAL authentication to use the custom user account factory.

Confirm that the the Program file file uses the Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

The example in this section builds on the approach of reading the base URL with version and scopes from app configuration via the MicrosoftGraph section in wwwroot/appsettings.json file. The following lines should already be present in the Program file from following the guidance earlier in this article:

var baseUrl = builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"];
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>();

builder.Services.AddGraphClient(baseUrl, scopes);

In the Program file, find the call to the AddMsalAuthentication extension method. Update the code to the following, which includes a call to AddAccountClaimsPrincipalFactory that adds an account claims principal factory with the CustomAccountFactory.

If the app uses a custom user account class that extends RemoteUserAccount, swap the custom user account class for RemoteUserAccount in the following code.

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();

You can use the following UserClaims component to study the user’s claims after the user authenticates with ME-ID:

UserClaims.razor:razorCopy

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
@attribute [Authorize]

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

When testing with the Graph SDK locally, we recommend using a new in-private/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID.

Last modified: October 13, 2023

Author

Comments

Write a Reply or Comment