Subsections of Access control

Role-based access control

It’s common to use role-based access control, so even though it we recommend you to use permission-based access control, we will show what needs to be done for the application to check if a user belongs to a role (called group in Authway). To include the groups a user belongs to, you are required to add roles scope to your request.

HTTP/1.1 302 Found
Location: https://YOURINSTANCE.irmciam.se/connect/authorize?
          client_id=YOUR_CLIENT_ID
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback
          &response_type=code
          &scope=openid roles
          &state=YOUR_STATE

When doing so you will recieve one role claim for each group the user belongs too.

Even though this is straightforward to start with, there are several challenges with the role-based model that often result in many changes related to access control. For many features, it is common that many groups should have access, leading to very large if-statements. In a multi-tenant system (like the IdP, where each customer, reseller, partner, your own organisation itself, etc., is its own tenant), it’s also challenging to guarantee the names of the groups. For this, there is a concept of built-in group in IdP that cannot be renamed, and this must be used for role-based access control.

Example of using role-based access control

C# example

To include the roles a user belongs to, an additional scope and a claim-action must be added to the AddOpenIdConnect configuration:

options.Scope.Add("roles"); //Authway groups
options.ClaimActions.MapJsonKey("role", "role");

options.MapInboundClaims = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
    NameClaimType = "name", //Or your preferred claim for the user name
    RoleClaimType = "role"
};

With this additional configuration, claims of the type “role” will be retrieved, and since we had set RoleClaimType to “role” above, it means that the standard ways to check role membership will now work:

if (User.IsInRole("admin"))
    ...

eller:

[Authorize(Roles = "admin")]

Tips: IdentityModel is a package that has constants for many Claims which is a better way than using hardcoded strings as the examples above.

Permission-based access control

A system is built up of one or several functionalities. These functionalities are stable over time, meaning they exist as long as they are in the system, and they are never affected by organizational changes. Therefore, Authway has a concept of defining functionalities to which groups (or users) are granted access.

It is up to the system to define which functionalities need access control:

  • The system is a single functionality, meaning you either use the system or you don’t. It can also sometimes be divided into the right to read or the right to create/update/delete (administer).
  • One functionality per screen in the system.
  • One functionality for individual buttons in the system. An example could be a report, where one might have access for viewing (equivalent to a screen), but additionally has an extra functionality in the form of Export, where the content of the report can be exported. This could be more sensitive and therefore something one might want to control access for. In this case, you set up a function for “View Report X” and a function for “Export Report X”.

Functionalities can be managed directly in the Authway UI (User Interface), provided one has access to the “Manage Modules” functionality.

To include the permissions a user has, you are required to add perms scope to your request.

HTTP/1.1 302 Found
Location: https://YOURINSTANCE.irmciam.se/connect/authorize?
          client_id=YOUR_CLIENT_ID
          &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback
          &response_type=code
          &scope=openid perms
          &state=YOUR_STATE

When doing so you will recieve one perm claim for each functionality the user has access too.

Permission-based access control with .NET

Permission-based access control can be used just as easily in .NET as role-based access control. To enable this, we need to replace one of the build-in services and create an extension method. Below is example code for .NET how to implement permission-based access control.

To demonstrate how this can be used, we have defined a single functionality for an Example application, which is “Manage Order,” and we have given it the authorization name “Example.ManageOrder” in Authway. For this, we define the following additions in the Constants class:

public static class Constants
{
    public static class Permissions
    {
        public const string ManageOrder = "Example.ManageOrder";
    }
}

Now we need to expand/change which scopes are to be retrieved from the IdP and simultaneously define claim actions in the AddOpenIdConnect configuration:

options.Scope.Add("perms"); //Authway permissions
options.ClaimActions.MapJsonKey("perm", "perm"); //Authway permission claim

To make it easy to check if a user has authorization for a function in the system, for example, the following extension method can be created:

public static class PrincipalExtensions
{
    public static bool Authorize(this ClaimsPrincipal principal, string permission)
    {
        return principal.Identity.IsAuthenticated && principal.HasClaim(c => c.Type == "perm" && c.Value == permission);
    }
}

With the extension method in place we can easily show/hide the link to Manage order page (in _Layout.cshtml) based on the user’s access:

@if (User.Authorize(Constants.Permissions.ManageOrder))
{ 
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="ManageOrder">ManageOrder</a>
</li>
}

Certainly, we can check if a user has access in other code in the same way. However, it would be nice if we could use the Authorize attribute as well, so we could do something like this in our OrderController:

[Authorize(Constants.Permissions.ManageOrder)]
public IActionResult Index()
{
    return View();
}

In .NET Core/.NET 5 and later, Microsoft has finally realized that role-based access control might not be ideal and, as a result, introduced an authorization system based on policies (read more here: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies). To make it super easy to use our permission-based access control, we will create a custom PolicyProvider that automatically generates a policy to check authorization for a functionality, assuming the application hasn’t already defined a policy with that name:

/// <summary>
/// A policy provider that translates a policy name to a required permission with the same name, if not 
/// <see cref="DefaultAuthorizationPolicyProvider"/> already has provided a policy.
/// </summary>
public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    /// <summary>
    /// Creates a new instance of <see cref="PermissionAuthorizationPolicyProvider" />.
    /// </summary>
    /// <param name="options">The options used to configure this instance.</param>
    public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
    {
    }

    /// <summary>Gets the default authorization policy or a permission based policy if no default is configured.</summary>
    /// <returns>The authorization policy.</returns>
    public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        var policy = await base.GetPolicyAsync(policyName);

        if (policy == null)
        {
            policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .RequireClaim("perm", policyName)
                .Build();
        }

        return policy;
    }
}

Finally we need to tell .NET to use our PolicyProvider instead of DefaultAuthorizationPolicyProvider, and we do this like any other configuration in Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    //Add policy provider that generates a policy for each permission (if a policy is not found)
    services.AddTransient<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
}

With that configuration, we can now use the Authorize attribute as mentioned above.