Secure your webapp with RBAC using KeyCloak and .NET 6

I’ve spent a bit of time working with KeyCloak lately. It’s been some time since I looked in the Open Source world for an OIDC/OAuth2 solution, and when I found KeyCloak, I thought, “How did I miss this?”. I’ve been working with an ancient OIDC framework available for .NET, whose name escapes me right now. Later on, I came across IdentityServer4, now IdentityServer5, available as Duende IdentityServer under commercial license.

But KeyCloak was developed quietly by Red Hat, and seems to have gained some traction. Indeed, it is a highly capable authentication server, supporting the OIDC protocol, SSO complete with user provisioning via SCIM, and a complete OAuth2 implementation for more advanced scenarios. For this article, I’ll discuss the more basic approach of RBAC using KeyCloak and the built-in authn/authz available in .NET 6.

Install KeyCloak

KeyCloak is relatively easy to install:

  1. Download the binary package from https://www.keycloak.org/downloads.html
  2. Unzip/untar the binary package in a system directory somewhere
  3. Create a service that launches the KeyCloak server from the distribution

I’ll give instructions for Linux, but I imagine it should work equally well on any machine with a reasonably recent version of Java. I untarred under /opt, which creates a directory /opt/keycloak-20.01. I link this to /opt/keycloak.

We have to bootstrap the service before we can install it. We will need an initial username and password, and those will be set in the environment variables KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD respectively. Before we start that, though, we need to install and configure Java and MySQL. I’ll leave this for the reader, as it’s usually just a simple matter of installing the openjdk-18-jdk and mysql-server packages.

Next, we need to modify /opt/keycloak/conf/keycloak.conf as follows:

# postgres is supported too if you prefer
db=mysql

# Create this user
db-username=keycloak
db-password=keycloak

# The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor.
db-url=jdbc:mysql://localhost:3306/keycloak

# HTTPS - requires root privileges or change of port
https-protocols=TLSv1.3,TLSv1.2
https-port=443

# The file path to a server certificate or certificate chain in PEM format.
https-certificate-file=${kc.home.dir}/conf/server.crt.pem

# The file path to a private key in PEM format.
https-certificate-key-file=${kc.home.dir}/conf/server.key.pem

Start the service manually as follows:

# KEYCLOAK_ADMIN=admin KEYCLOACK_ADMIN_PASSWORD=changeme /opt/keycloak/bin/kc.sh start

This will enable you to login to the service at http://localhost:8080 with the username and password set in the environment. Your first order of business should be to create a new administrative user with a secure password, and disable the admin user.

You can now stop the running service (Ctrl-C in the terminal in which you ran the kc.sh command. It is time to replace it with a proper service file and have KeyCloak start automatically on boot.

# vi /etc/systemd/system/keycloak.service

Use the following contents:

[Unit]
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=root
ExecStart=/opt/keycloak/bin/kc.sh start

[Install]
WantedBy=multi-user.target

Finally, set KeyCloak to auto-start:

# systemctl enable --now keycloak.service

Configure KeyCloak

The first thing we will need is a new realm to manage our application’s users. Create a realm by logging into the server (hopefully you took the time to configure HTTPS!). In the top left corner, there is a dropdown that will list all the current realms:

You can add a new realm from the button on the bottom. Give it a name, and save it. This will bring you to the main configuration screen for your new realm:

There’s a lot here to configure, but don’t worry about most of it for now. A lot of the options are related to security policies and automatic enforcement of scopes and permissions using OAuth2 Resource Server flow. This is an advanced topic that this article will not cover.

For our purposes, we will configure just the name. We will use the Client settings to configure our RBAC. So, create a new Client by selecting Clients on the left navigation, and clicking Create. Fill in a name, leave the protocol on openid-connect. You don’t need to fill in the Root URL, but you can if you like.

Now you are at the main configuration screen for your new client:

We are only interested in roles. Go to the Roles tab and add any roles you might need (I used Administrator and User, making Administrator a composite role that contained User as well). You can then assign these roles to individual users in their details screen.

So adding users with roles is easy enough. How do we inform our application of those roles? We need to put a claim in the access token that will declare our roles to the application. KeyCloak’s built-in mapper for User Client Role will put those roles in a JSON block within the token as follows:

resource_access: {
    bookstore: {
        [ "Administrator", "User" ]
    }
}

Unfortunately, .NET 6 won’t interpret these roles out-of-the-box, so we need to give it a little help. Help is provided in the form of a class extending AccountClaimsPrincipalFactory<RemoteUserAccount>. The base class provides a virtual method, CreateUserAsync(), that will construct the ClaimsIndentity given an access token (well, more specifically a token accessor – more on that below). The entire class looks like this:

public class KeycloakClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    public KeycloakClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var user = await base.CreateUserAsync(account, options);
        if (user.Identity is ClaimsIdentity identity)
        {

            var tokenRequest = await TokenProvider.RequestAccessToken();
            if (tokenRequest.Status == AccessTokenResultStatus.Success)
            {
                if (tokenRequest.TryGetToken(out var token))
                {
                    var handler = new JwtSecurityTokenHandler();
                    var parsedToken = handler.ReadJwtToken(token.Value);
                    var json = parsedToken.Claims.SingleOrDefault(c => c.Type == "resource_access");
                    if (json?.Value != null)
                    {
                        var obj = JsonConvert.DeserializeObject<dynamic>(json.Value);
                        var roles = (JArray?) obj?["bookstore"]["roles"];
                        if (roles != null)
                            foreach (var role in roles)
                                identity.AddClaim(new Claim(ClaimTypes.Role, role.ToString()));
                    }
                }
            }
        }
        return user;
    }

Note that we use the TokenProvider provided by the base class. This is an IAccessTokenProvider, which will use the IdP token endpoint to fetch a fresh access token. This is important to note, because if we are not yet authenticated, we obviously cannot get an access token, hence the need to ensure that we are receiving a valid token response prior to proceeding.

The key line here is var roles = (JArray?) obj?["bookstore"]["roles"]. A JArray works very much like a Javascript Array, and can dereference multiple levels of a hierarchy using array notation. Once we have the roles, we simply add the claim to the identity using the expected claim type and return the updated identity.

Now that we have an access token with the proper claims, we should be able to simply use the following service declaration:

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("OIDC", options.ProviderOptions);
}).AddAccountClaimsPrincipalFactory<KeycloakClaimsPrincipalFactory>();

(Note – this is Blazor WASM. You will need to use an appropriate package for Blazor Server to do the same thing). You will also need an appsettings.json with the following content:

"OIDC": {
    "Authority": "https://dev-keycloak.example.com/realms/Bookstore/",
    "ClientId": "bookstore-dev",
    "RedirectUri": "https://localhost:7004/authentication/login-callback",
    "ResponseType": "code"
  }

Now the final step is to use AuthorizeAttributes to control access:

@attribute [Authorize(Roles = "Administrator")]

That’s it! You can now use the .NET 6 RBAC to interact with KeyCloak.

Advertisement
,

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 )

Facebook photo

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

Connecting to %s

%d bloggers like this: