Using Azure security groups in ASP.NET Core with an Azure B2C Identity Provider

This article shows how to implement authorization in an ASP.NET Core application which uses Azure security groups for the user definitions and Azure B2C to authenticate. Microsoft Graph API is used to access the Azure group definitions for the signed in user. The client credentials flow is used to authorize the Graph API client with an application scope definition. This is not optimal, the delegated user flows would be better. By allowing applications rights for the defined scopes using Graph API, you are implicitly making the application an administrator of the tenant as well for the defined scopes.

Code: https://github.com/damienbod/azureb2c-fed-azuread

Two Azure AD security groups were created to demonstrate this feature with Azure B2C authentication. The users were added to the admin group and the user group as required. The ASP.NET Core application uses an ASP.NET Core Razor page which should only be used by admin users, i.e. people in the group. To validate this in the application, Microsoft Graph API is used to get groups for the signed in user and an ASP.NET Core handler, requirement and policy uses the group claim created from the Azure group to force the authorization.

The groups are defined in the same tenant as the Azure B2C.

A separate Azure App registration is used to define the application Graph API scopes. The User.Read.All application scope is used. In the demo, a client secret is used, but a certificate can also be used to access the API.

The Microsoft.Graph Nuget package is used as a client for Graph API.

<PackageReference Include="Microsoft.Graph" Version="4.4.0" />

The GraphApiClientService class implements the Microsoft Graph API client. A ClientSecretCredential instance is used as the AuthProvider and the definitions for the client are read from the application configurations and the user secrets in development, or Azure Key Vault. The user-id from the name identifier claim is used to get the Azure groups for the signed-in user. The claim namespaces gets added using the Microsoft client, this can be deactivated if required. I usually use the default claim names but as the is an Azure IDP, I left the Microsoft defaults which adds the extra stuff to the claims. The Graph API GetMemberGroups method returns the group IDs for the signed in identity.

using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;
using System.Threading.Tasks;

namespace AzureB2CUI.Services
{
    public class GraphApiClientService
    {
        private readonly GraphServiceClient _graphServiceClient;

        public GraphApiClientService(IConfiguration configuration)
        {
            string[] scopes = configuration.GetValue<string>("GraphApi:Scopes")?.Split(' ');
            var tenantId = configuration.GetValue<string>("GraphApi:TenantId");

            // Values from app registration
            var clientId = configuration.GetValue<string>("GraphApi:ClientId");
            var clientSecret = configuration.GetValue<string>("GraphApi:ClientSecret");

            var options = new TokenCredentialOptions
            {
                AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
            };

            // https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
            var clientSecretCredential = new ClientSecretCredential(
                tenantId, clientId, clientSecret, options);

            _graphServiceClient = new GraphServiceClient(clientSecretCredential, scopes);
        }

        public async Task<IDirectoryObjectGetMemberGroupsCollectionPage> GetGraphApiUserMemberGroups(string userId)
        {
            var securityEnabledOnly = true;

            return await _graphServiceClient.Users[userId]
                .GetMemberGroups(securityEnabledOnly)
                .Request().PostAsync()
                .ConfigureAwait(false);
        }
    }
}


The .default scope is used to access the Graph API using the client credential client.

"GraphApi": {
    "TenantId": "f611d805-cf72-446f-9a7f-68f2746e4724",
    "ClientId": "1d171c13-236d-4c2b-ac10-0325be2cbc74",
    "Scopes": ".default"
    //"ClientSecret": "--in-user-settings--"
},

The user and the application are authenticated using Azure B2C and an Azure App registration. Using Azure B2C, only a certain set of claims can be returned which cannot be adapted easily. Once signed-in, we want to include the Azure security group claims in the claims principal. To do this, the Graph API is used to find the claims for the user and add the claims to the claims principal using the IClaimsTransformation implementation. This is where the GraphApiClientService is used.

using AzureB2CUI.Services;
using Microsoft.AspNetCore.Authentication;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace AzureB2CUI
{
    public class GraphApiClaimsTransformation : IClaimsTransformation
    {
        private GraphApiClientService _graphApiClientService;

        public GraphApiClaimsTransformation(GraphApiClientService graphApiClientService)
        {

            _graphApiClientService = graphApiClientService;
        }

        public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            ClaimsIdentity claimsIdentity = new ClaimsIdentity();
            var groupClaimType = "group";
            if (!principal.HasClaim(claim => claim.Type == groupClaimType))
            {
                var nameidentifierClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
                var nameidentifier = principal.Claims.FirstOrDefault(t => t.Type == nameidentifierClaimType);

                var groupIds = await _graphApiClientService.GetGraphApiUserMemberGroups(nameidentifier.Value);

                foreach (var groupId in groupIds.ToList())
                {
                    claimsIdentity.AddClaim(new Claim(groupClaimType, groupId));
                }
            }

            principal.AddIdentity(claimsIdentity);
            return principal;
        }

The startup class adds the services and the authorization definitions for the ASP.NET Core Razor page application. The IsAdminHandlerUsingAzureGroups authorization handler is added and this is used to validate the Azure security group claim.

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<AdminApiService>();
	services.AddTransient<UserApiService>();
	services.AddScoped<GraphApiClientService>();
	services.AddTransient<IClaimsTransformation, GraphApiClaimsTransformation>();
	services.AddHttpClient();

	services.AddOptions();

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

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C")
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		.AddInMemoryTokenCaches();

	services.AddRazorPages().AddMvcOptions(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	}).AddMicrosoftIdentityUI();

	services.AddSingleton<IAuthorizationHandler, IsAdminHandlerUsingAzureGroups>();

	services.AddAuthorization(options =>
	{
		options.AddPolicy("IsAdminPolicy", policy =>
		{
			policy.Requirements.Add(new IsAdminRequirement());
		});
	});
}

The IsAdminHandlerUsingAzureGroups implements the AuthorizationHandler class with the IsAdminRequirement requirement. This handler checks for the administrator group definition from the Azure tenant.

using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace AzureB2CUI.Authz
{
    public class IsAdminHandlerUsingAzureGroups : AuthorizationHandler<IsAdminRequirement>
    {
        private readonly string _adminGroupId;

        public IsAdminHandlerUsingAzureGroups(IConfiguration configuration)
        {
            _adminGroupId = configuration.GetValue<string>("AzureGroups:AdminGroupId");
        }
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsAdminRequirement requirement)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));
            if (requirement == null)
                throw new ArgumentNullException(nameof(requirement));

            var claimIdentityprovider = context.User.Claims.FirstOrDefault(t => t.Type == "group"
                && t.Value == _adminGroupId);

            if (claimIdentityprovider != null)
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

The policy for this can be used anywhere in the application.

[Authorize(Policy = "IsAdminPolicy")]
[AuthorizeForScopes(Scopes = new string[] { "https://b2cdamienbod.onmicrosoft.com/5f4e8bb1-3f4e-4fc6-b03c-12169e192cd7/access_as_user" })]
public class CallAdminApiModel : PageModel
{

If a user tries to call the Razor page which was created for admin users, then an Access denied is returned. Of course, in a real application, the menu for this would also be hidden if the user is not an admin and does not fulfil the policy.

If the user is an admin and a member of the Azure security group, the data and the Razor page can be opened and viewed.

By using Azure security groups, it is really easily for IT admins to add or remove users from the admin role. This can be easily managed using Powershell scripts. It is a pity that Microsoft Graph API is required to use the Azure security groups when authenticating using Azure B2C. This is much more simple to use when authenticating using Azure AD.

Links

Managing Azure B2C users with Microsoft Graph API

https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/graph-api

https://docs.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#client-credentials-provider

https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview

https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-azure-ad-single-tenant

https://github.com/AzureAD/microsoft-identity-web

https://docs.microsoft.com/en-us/azure/active-directory/develop/microsoft-identity-web

https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-local

https://docs.microsoft.com/en-us/azure/active-directory/

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c

https://github.com/azure-ad-b2c/azureadb2ccommunity.io

https://github.com/azure-ad-b2c/samples

3 comments

  1. […] Using Azure security groups in ASP.NET Core with an Azure B2C Identity Provider – Damien Bowden […]

  2. […] Using Azure security groups in ASP.NET Core with an Azure B2C Identity Provider (Damien Bowden) […]

  3. […] Roadmap (Aleksandra Aganezova) Converting DOCX Form Fields to Smart HTML Forms (Bjoern Meyer) Using Azure security groups in ASP.NET Core with an Azure B2C Identity Provider (Damien Bowden) Building a Micro Web API with Azure Functions and SQLite (Jeremy Morgan) Analyzing […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: