EntraPolicyBasedAuthorization 1.0.1
dotnet add package EntraPolicyBasedAuthorization --version 1.0.1
NuGet\Install-Package EntraPolicyBasedAuthorization -Version 1.0.1
<PackageReference Include="EntraPolicyBasedAuthorization" Version="1.0.1" />
paket add EntraPolicyBasedAuthorization --version 1.0.1
#r "nuget: EntraPolicyBasedAuthorization, 1.0.1"
// 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.
- Groups.ReadAll
- 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
Update your project to .NET Core 8 (if necessary)
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
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();
});
- 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.
- 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>
}
- 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();
}
- 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.
- Test your application.
Add your own access policies based on Entra group
- Create a new Group in Entra.
- 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"
},
- Register a new policy into services container
services.AddAuthorizationBuilder()
.AddPolicy("Dashboard", policy =>
{
policy.Requirements.Add(new IsDashboardUserRequirment("Dashboard"));
});
- Add a new requirment class marked with special interface
public class IsDashboardUserRequirment : IAuthorizationRequirement
{
public string GroupName { get; }
public IsDashboardUserRequirment(string groupName)
{
GroupName = groupName;
}
}
- 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));
}
}
- Register a new created policy into a service container
services.AddSingleton<IAuthorizationHandler, DashboardUserHandler>();
- 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:
- Groups.ReadAll
- Users.ReadAll
Product | Versions 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. |
-
net8.0
- Microsoft.AspNetCore.Authentication.Abstractions (>= 2.1.1)
- Microsoft.AspNetCore.Authorization (>= 8.0.8)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.1)
- Microsoft.Graph (>= 5.57.0)
- Microsoft.Identity.Web (>= 3.1.0)
- Microsoft.Identity.Web.UI (>= 3.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Code reorganized. Documentation added.