Keycloak as Authorization Server in .NET

Oleksii Nikiforov - Dec 29 '22 - - Dev Community

Image description

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)
    • /endpoint1 - required ASP.NET Core identity role
    • /endpoint2 - required realm role
    • /endpoint3 - required client role
  • Remote authorization policy check executed by Authorization Server (Keycloak)
    • /endpoint4 - 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();

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Entry point:

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

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

Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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: Audiencetest-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

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"
  }
}

Enter fullscreen mode Exit fullscreen mode

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": ""
}

Enter fullscreen mode Exit fullscreen mode

💡 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))
});

Enter fullscreen mode Exit fullscreen mode

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));

Enter fullscreen mode Exit fullscreen mode

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:

Image description

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));

Enter fullscreen mode Exit fullscreen mode

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.

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.

Enter fullscreen mode Exit fullscreen mode
authz-demo1

💡 Note, In this post, I’ve showed you how to protect the one resource known to the system, but it is actually possible to create resources 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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .