Using Keycloak in .NET Aspire projects dotnet aspnetcore keycloak
Announcement - Keycloak.AuthServices v2.3.0 is out šŸŽ‰! aspnetcore dotnet keycloak auth
Announcement - Keycloak.AuthServices v2.0.0 is out šŸŽ‰! aspnetcore dotnet keycloak auth
Use Keycloak as Identity Provider from Blazor WebAssembly (WASM) applications aspnetcore dotnet auth keycloak

TL;DR

Keycloak.AuthService.Authorization provides a toolkit to use Keycloak as Authorization Server. An authorization Server is a powerful abstraction that allows to control authorization concerns. An authorization Server is also advantageous in microservices scenario because it serves as a centralized place for IAM and access control.

Keycloak.AuthServices.Authorization: https://github.com/NikiforovAll/keycloak-authorization-services-dotnet#keycloakauthservicesauthorization

Example source code: https://github.com/NikiforovAll/keycloak-authorization-services-dotnet/tree/main/samples/AuthZGettingStarted

Introduction

Authorization refers to the process that determines what a user is able to do.ASP.NET Core authorization provides a simple, declarative role and a rich policy-based model. Authorization is expressed in requirements, and handlers evaluate a userā€™s claims against requirements. Imperative checks can be based on simple policies or policies which evaluate both the user identity and properties of the resource that the user is attempting to access.

Resource servers (applications or services serving protected resources) usually rely on some kind of information to decide if access should be granted to a protected resource. For RESTful-based resource servers, that information is usually obtained from a security token, usually sent as a bearer token on every request to the server. For web applications that rely on a session to authenticate users, that information is usually stored in a userā€™s session and retrieved from there for each request.

Keycloak is based on a set of administrative UIs and a RESTful API, and provides the necessary means to create permissions for your protected resources and scopes, associate those permissions with authorization policies, and enforce authorization decisions in your applications and services.

Considering that today we need to consider heterogeneous environments where users are distributed across different regions, with different local policies, using different devices, and with a high demand for information sharing, Keycloak Authorization Services can help you improve the authorization capabilities of your applications and services by providing:

  • Resource protection using fine-grained authorization policies and different access control mechanisms
  • Centralized Resource, Permission, and Policy Management
  • Centralized Policy Decision Point
  • REST security based on a set of REST-based authorization services
  • Authorization workflows and User-Managed Access
  • The infrastructure to help avoid code replication across projects (and redeploys) and quickly adapt to changes in your security requirements.

Example Overview

In this blog post I will demonstrate how to perform authorization in two ways:

  • Role-based access control (RBAC) check executed by Resource Server (API)
    • /endpoint - required ASP.NET Core identity role
    • /endpoint - required realm role
    • /endpoint - required client role
  • Remote authorization policy check executed by Authorization Server (Keycloak)
    • /endpoint - remotely executed policy selected for ā€œworkspaceā€ - resource, ā€œworkspaces:readā€ - scope.
var app = builder.Build();

app
    .UseHttpsRedirection()
    .UseApplicationSwagger(configuration)
    .UseAuthentication()
    .UseAuthorization();

app.MapGet("/endpoint1", (ClaimsPrincipal user) => user)
    .RequireAuthorization(RequireAspNetCoreRole);

app.MapGet("/endpoint2", (ClaimsPrincipal user) => user)
    .RequireAuthorization(RequireRealmRole);

app.MapGet("/endpoint3", (ClaimsPrincipal user) => user)
    .RequireAuthorization(RequireClientRole);

app.MapGet("/endpoint4", (ClaimsPrincipal user) => user)
    .RequireAuthorization(RequireToBeInKeycloakGroupAsReader);

await app.RunAsync();

Project structure:

$ tree -L 2
.
ā”œā”€ā”€ AuthZGettingStarted.csproj
ā”œā”€ā”€ Program.cs
ā”œā”€ā”€ Properties
ā”‚   ā””ā”€ā”€ launchSettings.json
ā”œā”€ā”€ ServiceCollectionExtensions.Auth.cs
ā”œā”€ā”€ ServiceCollectionExtensions.Logging.cs
ā”œā”€ā”€ ServiceCollectionExtensions.OpenApi.cs
ā”œā”€ā”€ appsettings.Development.json
ā”œā”€ā”€ appsettings.json
ā”œā”€ā”€ assets
ā”‚   ā”œā”€ā”€ realm-export.json
ā”‚   ā””ā”€ā”€ run.http
ā””ā”€ā”€ docker-compose.yml

Entry point:

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
var services = builder.Services;

builder.AddSerilog();
services
    .AddApplicationSwagger(configuration)
    .AddAuth(configuration);

Register AuthN and AuthZ services in Dependency Injection container:

public static IServiceCollection AddAuth(
    this IServiceCollection services, IConfiguration configuration)
{
    services.AddKeycloakAuthentication(configuration);

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            Policies.RequireAspNetCoreRole,
            builder => builder.RequireRole(Roles.AspNetCoreRole));

        options.AddPolicy(
            Policies.RequireRealmRole,
            builder => builder.RequireRealmRoles(Roles.RealmRole));

        options.AddPolicy(
            Policies.RequireClientRole,
            builder => builder.RequireResourceRoles(Roles.ClientRole));

        options.AddPolicy(
            Policies.RequireToBeInKeycloakGroupAsReader,
            builder => builder
                .RequireAuthenticatedUser()
                .RequireProtectedResource("workspace", "workspaces:read"));

    }).AddKeycloakAuthorization(configuration);

    return services;
}

public static class AuthorizationConstants
{
    public static class Roles
    {
        public const string AspNetCoreRole = "realm-role";

        public const string RealmRole = "realm-role";

        public const string ClientRole = "client-role";
    }

    public static class Policies
    {
        public const string RequireAspNetCoreRole = nameof(RequireAspNetCoreRole);

        public const string RequireRealmRole = nameof(RequireRealmRole);

        public const string RequireClientRole = nameof(RequireClientRole);

        public const string RequireToBeInKeycloakGroupAsReader = 
            nameof(RequireToBeInKeycloakGroupAsReader);
    }
}

Configure Keycloak

In this post Iā€™m going to skip basic Keycloak installation and configuration, please see my previous posts for more details https://nikiforovall.github.io/tags.html#keycloak-ref.

Prerequisites:

  1. Create a realm named: Test
  2. Create a user with username/password: user/user
  3. Create a realm role: realm-role
  4. Create a client: test-client
    1. Create an audience mapper: Audience āž” test-client
    2. Enable Client Authentication and Authorization
    3. Enable Implicit flow and add a valid redirect URL (used by Swagger to retrieve a token)
  5. Create a client role: client-role
  6. Create a group called workspace and add the ā€œuserā€ to it

Full Keycloak configuration (including the steps below) can be found at realm-export.json

Authorization based on ASP.NET Core Identity roles

Keycloak.AuthService.Authentication ads the KeycloakRolesClaimsTransformation that maps roles provided by Keycloak. The source for role claim could be one of the following:

  • Realm - map realm roles
  • ResourceAccess - map client roles
  • None - donā€™t map
flowchart LR AddKeycloakAuthentication --> AddAuthentication AddKeycloakAuthentication --> KeycloakRolesClaimsTransformation

Depending on your needs, you can use realm roles, client roles or skip automatic role mapping/transformation. The role claims transformation is based on the config. For example, here is how to use realms role for ASP.NET Core Identity roles. As result, you can use build-in role-based authorization.

{
  "Keycloak": {
    "realm": "Test",
    "auth-server-url": "http://localhost:8080/",
    "ssl-required": "none",
    "resource": "test-client",
    "verify-token-audience": true,
    "credentials": {
      "secret": ""
    },
    "confidential-port": 0,
    "RolesSource": "Realm"
  }
}

So, for a user with the next access token generated by Keycloak the roles are effectively evaluated to ā€œrealm-roleā€, ā€œdefault-roles-testā€, ā€œoffline_accessā€, ā€œuma_authorizationā€. And if you change ā€œRolesSourceā€ to ā€œResourceAccessā€ it would be ā€œclient-roleā€.

{
  "exp": 1672275584,
  "iat": 1672275284,
  "jti": "1ce527e6-b852-48e9-b27b-ed8cc01cf518",
  "iss": "http://localhost:8080/realms/Test",
  "aud": [
    "test-client",
    "account"
  ],
  "sub": "8fd9060e-9e3f-4107-94f6-6c3a242fb91a",
  "typ": "Bearer",
  "azp": "test-client",
  "session_state": "c32e4165-f9bd-4d4c-93bd-3847f4ffc697",
  "acr": "1",
  "realm_access": {
    "roles": [
      "realm-role",
      "default-roles-test",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "test-client": {
      "roles": [
        "client-role"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid email profile",
  "sid": "c32e4165-f9bd-4d4c-93bd-3847f4ffc697",
  "email_verified": false,
  "preferred_username": "user",
  "given_name": "",
  "family_name": ""
}

šŸ’” Note, you can change ā€œRolesSourceā€ to ā€œNoneā€ and instead of using KeycloakRolesClaimsTransformation, use Keycloak role claim mapper and populate role claim based on configuration. Luckily, it is easy to do from Keycloak admin panel.

services.AddAuthorization(options =>
{
    options.AddPolicy(
        Policies.RequireAspNetCoreRole,
        builder => builder.RequireRole(Roles.AspNetCoreRole))
});

Authorization based on Keycloak realm and client roles

AuthorizationPolicyBuilder allows to register policies and Keycloak.AuthServices.Authorization adds a handy method to register rules that make use of the specific structure of access tokens generated by Keycloak.

services.AddAuthorization(options =>
{
    options.AddPolicy(
        Policies.RequireRealmRole,
        builder => builder.RequireRealmRoles(Roles.RealmRole));

    options.AddPolicy(
        Policies.RequireClientRole,
        builder => builder.RequireResourceRoles(Roles.ClientRole));
});

// PoliciesBuilderExtensions.cs
public static AuthorizationPolicyBuilder RequireResourceRoles(
    this AuthorizationPolicyBuilder builder, params string[] roles) =>
    builder
        .RequireClaim(KeycloakConstants.ResourceAccessClaimType)
        .AddRequirements(new ResourceAccessRequirement(default, roles));

public static AuthorizationPolicyBuilder RequireRealmRoles(
    this AuthorizationPolicyBuilder builder, params string[] roles) =>
    builder
        .RequireClaim(KeycloakConstants.RealmAccessClaimType)
        .AddRequirements(new RealmAccessRequirement(roles));

Authorization based on Authorization Server permissions

Policy Enforcement Point (PEP) is responsible for enforcing access decisions from the Keycloak server where these decisions are taken by evaluating the policies associated with a protected resource. It acts as a filter or interceptor in your application in order to check whether or not a particular request to a protected resource can be fulfilled based on the permissions granted by these decisions.

Keycloak supports fine-grained authorization policies and is able to combine different access control mechanisms such as:

  • Attribute-based access control (ABAC)
  • Role-based access control (RBAC)
  • User-based access control (UBAC)
  • Context-based access control (CBAC)
  • Rule-based access control
  • Using JavaScript
  • Time-based access control
  • Support for custom access control mechanisms (ACMs) through a Service Provider Interface (SPI)

Here is what happens when authenticated user tries to access a protected resource:

sequenceDiagram participant User participant API participant AuthServer as Authorization Server - PEP User ->>+ API: access a protected endpoint API->>+ AuthServer : verify if user has an access to resource + scope AuthServer ->>- API: authorization result (yes/no) API ->>- User: response (2xx/403)
services.AddAuthorization(options =>
{
    options.AddPolicy(
        Policies.RequireToBeInKeycloakGroupAsReader,
        builder => builder
            .RequireAuthenticatedUser()
            .RequireProtectedResource("workspace", "workspaces:read"));
});

// PoliciesBuilderExtensions.cs
/// <summary>
/// Adds protected resource requirement to builder.
/// Makes outgoing HTTP requests to Authorization Server.
/// </summary>
public static AuthorizationPolicyBuilder RequireProtectedResource(
    this AuthorizationPolicyBuilder builder, string resource, string scope) =>
    builder.AddRequirements(new DecisionRequirement(resource, scope));

The power of Authorization Server - define policies and permissions

Resource management is straightforward and generic. After creating a resource server, you can start creating the resources and scopes that you want to protect. Resources and scopes can be managed by navigating to the Resource and Authorization Scopes tabs, respectively.

So, to define a protected resource we need to create it in the Keycloak and assigned scope to it. In our case, we need to create ā€œworkspaceā€ resource with ā€œworkspaces:readā€ scope.

For more details, please see https://www.keycloak.org/docs/latest/authorization_services/#_resource_overview.

PermissionsPoliciesABACRBACUBACCBACRuleBasedJavaScriptTimeBasedResourcesScopesDecision Strategy

To create a scope:

  1. Navigate to ā€œClientsā€ tab on the sidebar
  2. Select ā€œtest-clientā€ from the list
  3. Go to ā€œAuthorizationā€ tab (make sure you enabled ā€œAuthorizationā€ checkbox on the ā€œSettingsā€ tab)
  4. Select ā€œScopesā€ sub-tab
  5. Click ā€œCreate authorization scopeā€
  6. Specify workspaces:read as Name
  7. Click ā€œSaveā€

To create a resource:

  1. From the ā€œAuthorizationā€ tab
  2. Select ā€œResourcesā€ sub-tab
  3. Click ā€œCreate resourceā€
  4. Specify workspace as Name
  5. Specify urn:resource:workspace as Type
  6. Specify ā€œworkspaces:readā€ as ā€œAuthorization scopesā€
  7. Click ā€œSaveā€

Letā€™s say we want to implement a rule that only users with realm-role role and membership in workspace group can read a ā€œworkspaceā€ resource. To accomplish this, we need to create the next two policies:

  1. From the ā€œAuthorizationā€ tab
  2. Select ā€œPoliciesā€ sub-tab
  3. Click ā€œCreate policyā€
  4. Select ā€œRoleā€ option
  5. Specify Is in realm-role as Name
  6. Click ā€œAdd rolesā€
  7. Select realm-role role
  8. Logic: Positive
  9. Click ā€œSaveā€
  10. Click ā€œCreate policyā€
  11. Select ā€œGroupā€ option
  12. Specify Is in workspace group as Name
  13. Click ā€œAdd groupā€
  14. Select ā€œworkspaceā€ group
  15. Logic: Positive
  16. Click ā€œSaveā€

Now, we can create the permission:

  1. From the ā€œAuthorizationā€ tab
  2. Select ā€œPermissionsā€ sub-tab
  3. Click ā€œCreate permissionā€
  4. Select ā€œCreate resource-based permissionā€
  5. Specify Workspace Access as Name
  6. Specify workspace as resource
  7. Add workspaces:read as authorization scope
  8. Add two previously created policies to the ā€œPoliciesā€
  9. Specify Unanimous as Decision Strategy
  10. Click ā€œSaveā€

The decision strategy dictates how the policies associated with a given permission are evaluated and how a final decision is obtained. ā€˜Affirmativeā€™ means that at least one policy must evaluate to a positive decision in order for the final decision to be also positive. ā€˜Unanimousā€™ means that all policies must evaluate to a positive decision in order for the final decision to be also positive. ā€˜Consensusā€™ means that the number of positive decisions must be greater than the number of negative decisions. If the number of positive and negative is the same, the final decision will be negative.

Evaluate permissions

  1. From the ā€œAuthorizationā€ tab
  2. Select ā€œEvaluateā€ sub-tab

Letā€™s say the ā€œuserā€ has realm-role, but is not a member of workspace group

Here is how the permission evaluation is interpreted by Keycloak:

evaluate1

And if we add the ā€œuser to workspace group:

evaluate1

Demo

  1. Navigate at https://localhost:7248/swagger/index.html
  2. Click ā€œAuthorizeā€. Note, the access token is retrieved based on ā€œImplicit Flowā€ that weā€™ve previously configured.
  3. Enter credentials: ā€œuser/userā€
  4. Execute ā€œ/endpoint4ā€
authz-demo2

As you can see, the response is 200 OK. I suggest you to try removing ā€œuserā€ from the ā€œworkspaceā€ group and see how it works.

As described above, the permission is evaluated by Keycloak therefore you can see outgoing HTTP requests in the logs:

12:19:28 [INFO] Start processing HTTP request "POST" http://localhost:8080/realms/Test/protocol/openid-connect/token
"System.Net.Http.HttpClient.IKeycloakProtectionClient.ClientHandler"
12:19:28 [INFO] Sending HTTP request "POST" http://localhost:8080/realms/Test/protocol/openid-connect/token
"System.Net.Http.HttpClient.IKeycloakProtectionClient.ClientHandler"
12:19:28 [INFO] Received HTTP response headers after 8.5669ms - 200
"System.Net.Http.HttpClient.IKeycloakProtectionClient.LogicalHandler"
12:19:28 [INFO] End processing HTTP request after 25.3503ms - 200
"Keycloak.AuthServices.Authorization.Requirements.DecisionRequirementHandler"
12:19:28 [DBUG] ["DecisionRequirement: workspace#workspaces:read"] Access outcome True for user "user"
"Microsoft.AspNetCore.Authorization.DefaultAuthorizationService"
12:19:28 [DBUG] Authorization was successful.
authz-demo1

šŸ’” Note, In this post, Iā€™ve showed you how to protect a one resource known to the system, but it is actually possible to create resource programmatically and compose ASP.NET Core policies during runtime. See Keycloak.AuthServices.Authorization.ProtectedResourcePolicyProvider for more details.

Summary

An authorization Server is a highly beneficial abstraction and it is quite easy to solve a wide range of well-known problems without ā€œReinventing the wheelā€. Keycloak.AuthServices.Authorization helps you to define a protected resource and does the interaction with Authorization Server for you. Let me know what you think šŸ™‚

References


Oleksii Nikiforov

Jibber-jabbering about programming and IT.