EntraPolicyBasedAuthorization 1.0.1

dotnet add package EntraPolicyBasedAuthorization --version 1.0.1                
NuGet\Install-Package EntraPolicyBasedAuthorization -Version 1.0.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EntraPolicyBasedAuthorization" Version="1.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add EntraPolicyBasedAuthorization --version 1.0.1                
#r "nuget: EntraPolicyBasedAuthorization, 1.0.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install EntraPolicyBasedAuthorization as a Cake Addin
#addin nuget:?package=EntraPolicyBasedAuthorization&version=1.0.1

// Install EntraPolicyBasedAuthorization as a Cake Tool
#tool nuget:?package=EntraPolicyBasedAuthorization&version=1.0.1                

Overview

This library created to simplify EntraID B2C integration process to .NET Core WEB app. Authentication and policy based Authorization (with Entra groups) added. As a starting point used an article created by Jesiel Padilha on the Medium portal.

Authentication and Authorization in Azure (Entra ID) — Part 3

Start conditions

I suppose you already have a properly configured Entra B2C application. Authentication used "User Flows" from Entra. Authorization used "Groups" created in Entra. The library has two default pre-configured access policies "Admin" and "User".

Your application should have proper permissions in Entra.

  1. Groups.ReadAll
  2. Users.ReadAll

And, if you don't have an application in Entra, you can start with this tutorials:

Tutorial - Create an Azure Active Directory B2C tenant

Tutorial - Register a web application in Azure Active Directory B2C - Azure AD B2C

Tutorial - Create user flows and custom policies - Azure Active Directory B2C

Configure authentication in a sample web application by using Azure Active Directory B2C

Azure AD B2C web app documentation

How to use this

  1. Update your project to .NET Core 8 (if necessary)

  2. Add this library to your project using NuGet package manager or a console command:

    latest dotnet add package EntraPolicyBasedAuthorization

    specific version dotnet add package EntraPolicyBasedAuthorization --version 1.0.0

  3. In a "Startup.cs" file remove previous Authentication or/and Authorization. And, add next extension method in "ConfigureServices":

services.AddAzureB2cAuthentication(Configuration);

Next, in "Configure" method you should update your code related to "UseEndpoints" method, and redirection like shown below. If you have pretty default configuration you can just add an extension methods:

app.AddCustomAccessDeniedCodePage();
app.AddCustomUseEndpoints();

A way to update your code without extension methods. "UseAuthentication", "UseAuthorization" methods are necessary. "UseEndpoints" should contain "MapRazorPages":

    app.UseAuthentication();
    app.UseAuthorization();

    // this is optional if you want to redirect to a Forbidden page
    app.UseStatusCodePages(statusCodeContext =>
        {
            if (statusCodeContext.HttpContext.Request.Path.HasValue &&
                statusCodeContext.HttpContext.Request.Path.Value.Contains("/AccessDenied"))
            {
                statusCodeContext.HttpContext.Response.Redirect("/Home/Forbidden");
            }

            return Task.CompletedTask;
        });

    app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}")
            .RequireAuthorization();

            endpoints.MapRazorPages();
        });
  1. Then, add changes to a appsettings.json file:
    "AzureAdB2C": {
        "Instance": "https://your_domain_name.b2clogin.com",
        "Domain": "your_domain_name.onmicrosoft.com",
        "ClientId": "your_client_id_from_app_registration",
        "SignUpSignInPolicyId": "your_userflow_name_from_policy",
        "SourceType": "ClientSecret",
        "ClientSecret": "your_client_secret_from_app_registration"
    },
    
    "AzureGroups": {
        "Admin": "your_admin_group_ID_here_from_groups",
        "User": "your_user_group_ID_here_from_groups"
    },
    
    "CacheSettings": {
        "TimeoutInMinites": 30
    }

"Admin" and "User" names here can be different with Entra. For example you can use "MyPortalSystemAdmin" in Entra. And specify an ID of this group as a value for the "Admin" parameter. It is just more clear to use similar names.

Also these names used as a default access policies names in the library. So, it is necessary to have these two parameters and their IDs from Entra.

  1. Next, you should update your app to use login page and logout link. As example "_LoginPartial.cshtml" shown. With user groups list:
@using System.Security.Principal
@using EntraPolicyBasedAuthorization.Interfaces

@inject IGraphApiUserInfo UserInfo

<ul class="navbar-nav">
    @if (User.Identity is not null && User.Identity.IsAuthenticated)
    {
        if (User.Identity.IsAuthenticated)
        {
            var groups = await UserInfo.GetUserGroupsInfo(User);

            <li class="nav-item dropdown">
                <a class="nav-link dropdown-toggle font-weight-bold" href="#" id="navbarUserMenu" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">@User.Identity.Name</a>
                <div class="dropdown-menu" aria-labelledby="navbarUserMenu">
                    <a class="dropdown-item small disabled" href="#">User roles:</a>
                    @foreach (var item in groups)
                    {
                        <a class="dropdown-item disabled small" href="#" title="@item.Description">@item.DisplayName</a>
                    }
                    <a class="dropdown-item" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Logout</a>
                </div>
            </li>
        }
    }
</ul>

If necessary - update your application layout to show any navigation bars and so on only for an autenticated users. Something like this:

{
    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
        your layout 
    </div>
}
  1. Add a "Fobidden" method to a controller if you plan to use it. And, if you use "AddCustomAccessDeniedCodePage" extension method. Example (HomeController):
    public IActionResult Forbidden()
    {
        return View();
    }
  1. Mark controller methods with [Authorize] attribute. Like this:
    [Authorize(Policy = "Admin")]
    public IActionResult Dashboard()
    {
        return View();
    }

An attribute value must be equal to a policy name. Default two are "Admin" and "User". How to add your own described below.

  1. Test your application.

Add your own access policies based on Entra group

  1. Create a new Group in Entra.
  2. Add a new parameter in appsettings.json. This name you should use later as a policy name.
  "AzureGroups": {
    "Admin": "your_admin_group_ID_here_from_groups",
    "User": "your_admin_group_ID_here_from_groups",
    "Dashboard": "id_of_new_group_from_Entra"
  },
  1. Register a new policy into services container
    services.AddAuthorizationBuilder()
            .AddPolicy("Dashboard", policy =>
            {
                policy.Requirements.Add(new IsDashboardUserRequirment("Dashboard"));
            });
  1. Add a new requirment class marked with special interface
    public class IsDashboardUserRequirment : IAuthorizationRequirement
    {
        public string GroupName { get; }

        public IsDashboardUserRequirment(string groupName)
        {
            GroupName = groupName;
        }
    }
  1. Next, add a requirment handler class like this
public class DashboardUserHandler : AuthorizationHandler<IsDashboardUserRequirment>
{
    private readonly IConfiguration _configuration;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public DashboardUserHandler(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
    {
        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsDashboardUserRequirment requirement)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (requirement is null)
        {
            throw new ArgumentNullException(nameof(requirement));
        }

        if (IsInProperGroup(requirement.GroupName))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }

    private bool IsInProperGroup(string groupName)
    {
        if (_httpContextAccessor.HttpContext is null)
        {
            return false;
        }

        if (false == _httpContextAccessor.HttpContext.User.Identity?.IsAuthenticated)
        {
            return false;
        }

        var groupId = _configuration[$"{DefaultConstants.SettingsNameAzureGroups}:{groupName}"];

        return _httpContextAccessor.HttpContext!.User.Claims.Any(x => x.Type.Equals(DefaultConstants.ClaimTypeGroup) && x.Value.Equals(groupId));
    }
}
  1. Register a new created policy into a service container
    services.AddSingleton<IAuthorizationHandler, DashboardUserHandler>();
  1. Mark a controller method or a whole controller with "Authorize" attribute and test your application.
    [Authorize(Policy = "Dashboard")]
    public IActionResult Dashboard()
    {
        return View();
    }

NOTE: You could use other custom policies NOT based on Entra B2C groups. As an example check user age and so on. You have to collect this info in Entra. Examples of policies are here

Some typical errors

"Can't read string from PII"

Almost all times a parameter missed or wrong in the appsetings.json file. A parameter must have proper name. See samples in "how to use sections".

"Can't access or access violated"

Your application don't have access insed Entra. You have to setup this. Entra → Applications → App Registrations → Your Application → API Permissions At least two additional permissions should be added:

  1. Groups.ReadAll
  2. Users.ReadAll
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 102 9/19/2024
1.0.0 113 9/16/2024

Code reorganized. Documentation added.