Working With Roles
LightNap uses ASP.NET Roles to manage application authorization. By default there is only one role, Administrator
. However, you can extend the application to introduce new roles to support a variety of authorization needs.
Adding a Role
Roles are defined in Configuration/ApplicationRoles.cs
in the LightNap.Core
project. To add a new role, simply use the existing Administrator
role as a reference example. Roles use the custom ApplicationRole
instance that can be extended with additional properties, if required.
New roles must also be added to the ApplicationRoles.All
collection so that they can be managed on startup. The app automatically references this collection to add any roles that don’t yet exist and delete those that don’t exist anymore. It’s also the list that’s returned to the front-end for managing user membership in roles.
Here’s an example of how a new Moderator
role can be added:
public static class ApplicationRoles
{
public static readonly ApplicationRole Administrator = new(Constants.Roles.Administrator, "Administrator", "Access to all administrative features");
public static readonly ApplicationRole Moderator = new("Moderator", "Moderator", "Moderates content");
public static IReadOnlyList<ApplicationRole> All =>
[
Administrator,
Moderator
];
}
The “Moderator” name is defined in line here for simplicity, but you may prefer to add it to Constants.Roles
as a constant string in Configuration/Constants.cs
in the LightNap.Core
project.
Applying a Role Authorization
Suppose you wanted to update the application so that Moderator
users were able to hide comments via a hypothetical ChatController.HideComment()
endpoint. You can do this by specifying the authorized roles via the Authorize
attribute.
public class ChatController(IChatController chatService) : ControllerBase
{
...
[HttpPost("chat/{commentId}/hide")]
[Authorize(Roles = ["Moderator"])]
public async Task<ApiResponseDto<bool>> HideComment(string commentId)
{
...
That’s it. The role has been added and the REST endpoint now allows Moderator
access.
Allowing Any Of A List Of Roles
Sometimes you’ll want to allow users who are a member of any specified role to access an endpoint. This can be done by providing a list of roles in a single Authorize
attribute, such as:
[Authorize(Roles = ["Administrator,Moderator"])]
This will allow any user who is an Administrator
or Moderator
(or both) to access the endpoint.
Requiring All Of A List Of Roles
If your endpoint needs a user to be a member of all roles, add each as its own attribute, such as:
[Authorize(Roles = ["Administrator"])]
[Authorize(Roles = ["Moderator"])]
This will require a user to be both an Administrator
and a Moderator
to access the endpoint.
Learn more about role-based authorization in ASP.NET here.
Roles and JSON Web Tokens (JWTs)
Roles are automatically embedded in access tokens by the back-end to authorize future requests. As a result, tokens issued before a role assignment has changed will not reflect the latest role behavior until they are replaced. The maximum theoretical time for this is based on how long the access token is configured to expire in the application settings. Users can speed this up by logging out and in again.
Using Roles on the Front-End
Roles are automatically extracted from JWTs on the front-end, so there’s no additional work required to get them.
Accessing the Logged-In User’s Roles
The best way to access their roles is via IdentityService.watchLoggedInToRole$()
or IdentityService.watchLoggedInToAnyRole$()
. These return hot observables that publish every time the user’s roles have been updated from a new JWT. They’re backed by ReplaySubject
, so the response will be immediate if the roles have been set at least once.
Applying the Logged-In User’s Roles
The main benefit to using roles on the front-end is to show components available to a user belonging to a given role. There are some good patterns in place that can be referenced when applying behavior for new roles.
Route Guards
Route guards make it easy to determine whether a user can see a given route based on their role. A reference example for this is at app/core/guards/admin.guard.ts
. This guard watches for changes in roles and only allows the guarded section to be visible if the user is logged in as an Administrator
.
To protect a route, add the guard to its canActivate
collection. A reference example for this is in routing\routes.ts
where the admin
branch of the route tree is protected by the adminGuard
.
Front-end work to restrict access to functionality is superficial. While it provides a nicer user experience to show or hide components, the key security considerations must be taken care of on the back-end. Never rely on front-end security for anything meaningful because insecure back-end APIs can be easily exploited.
Role Directives
Route directives make it easy to show or hide elements based on the logged-in user’s role membership.
The showByRoles
directive only shows the element if the user is logged in and belongs to at least one of the roles listed.
<p-button showByRoles roles="Administrator" ...
or
<p-button showByRoles [roles]="['Administrator', 'Moderator']" ...
Similarly, the hideByRoles
directive only hides the element if the user is logged in and belongs to at least one of the roles listed.
<p-button hideByRoles roles="Administrator" ...
or
<p-button hideByRoles [roles]="['Administrator', 'Moderator']" ...
Menus
You can also change the sidebar menu based on the roles of the logged-in user.
Managing User Roles
Most administrative tasks should be covered by the built-in administrative functionality. By default, any Administrator
can add and remove users from the roles registered on the back-end. Changes to roles are picked up automatically, so running the application with the changes above will now show the new Moderator
role.