Location>code7788 >text

NET Cloud Native Application Practice (IV): Keycloak-based Authentication and Authorization

Popularity:874 ℃/2024-10-28 23:21:14

Objectives of the chapter
  1. Complete local deployment and configuration of Keycloak
  2. Complete integration with Keycloak at the Stickers RESTful API level
  3. Implementing Authentication and Authorization on the Stickers RESTful API

Local Deployment of Keycloak

The easiest way to deploy Keycloak locally is to use Docker. you can build the Dockerfile according to the official documentation and then run it directly using Docker Compose. Since Keycloak is also part of the infrastructure, it can be added directly to the file we used in the previous talk. Again, create a new folder for keycloak in the docker folder and then create a new Dockerfile with the following contents:

FROM /keycloak/keycloak:26.0 AS builder

# Enable health and metrics support
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true

# Configure a database vendor
ENV KC_DB=postgres

WORKDIR /opt/keycloak
# for demonstration purposes only, please make sure to use proper certificates in production instead
RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/
RUN /opt/keycloak/bin/ build

FROM /keycloak/keycloak:26.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/

ENTRYPOINT ["/opt/keycloak/bin/"]

Then modify the file to include a new service called stickers-keycloak:

stickers-keycloak:
  image: daxnet/stickers-keycloak:dev
  build:
    context: ./keycloak
    dockerfile: Dockerfile
  environment:
    - KC_DB=postgres
    - KC_DB_USERNAME=postgres
    - KC_DB_PASSWORD=postgres
    - KC_DB_SCHEMA=public
    - KC_DB_URL=jdbc:postgresql://stickers-pgsql:5432/stickers_keycloak?currentSchema=public
    - KC_HOSTNAME=localhost
    - KC_HOSTNAME_PORT=5600
    - KC_HTTP_ENABLED=true
    - KC_HOSTNAME_STRICT=false
    - KC_HOSTNAME_STRICT_HTTPS=false
    - KC_PROXY=edge
    - KC_BOOTSTRAP_ADMIN_USERNAME=admin
    - KC_BOOTSTRAP_ADMIN_PASSWORD=admin
    - QUARKUS_TRANSACTION_MANAGER_ENABLE_RECOVERY=true
  command: [
      'start',
      '--optimized'
  ]
  depends_on:
    - stickers-pgsql
  ports:
    - "5600:8080"

Of these environment variables, theKC_DBSpecifies the type of database to use for Keycloak, we are going to reuse the PostgreSQL database we used in the previous lecture, so fill in thepostgresKC_DB_USERNAMEKC_DB_PASSWORDKC_DB_SCHEMArespond in singingKC_DB_URLSpecifies the database username, password, schema name, and database connection string.KC_HOSTNAMEKC_HOSTNAME_PORTspecifies the hostname and port number on which Keycloak will run, which needs to be the same as the port number of theportsThe external port number specified in theKC_BOOTSTRAP_ADMIN_USERNAMErespond in singingKC_BOOTSTRAP_ADMIN_PASSWORDSpecifies the Keycloak default administrator name and password.

Before starting Keycloak, you also need to prepare the PostgreSQL database. Keycloak automatically connects to the database and creates database objects (tables, fields, relationships, etc.) when it starts. Preparing the database is also very simple. Continuing with the method described in the previous lecture, when building the PostgreSQL database image, copy the SQL file that creates the database to the image's/folder is sufficient. the SQL file contains the following:

SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

CREATE DATABASE stickers_keycloak WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE_PROVIDER = libc LOCALE = 'en_US.utf8';
ALTER DATABASE stickers_keycloak OWNER TO postgres;

Then rebuild and run the PostgreSQL and Keycloak containers:

$ docker compose -f  build
$ docker compose -f  up

It is highly recommended that before rebuilding and running the container, you clear the localstickers-pgsql:devMirroring and deletingdocker_stickers_postgres_datavolumes to ensure that the old data does not affect the new deployment.

After successfully launching the container, open a browser and visit http://localhost:5600 and you should be able to open Keycloak's main page and log in with admin/admin.

Configuring Stickers Realm in Keycloak

Due to space constraints, I'm not going to show the entire process of configuring Keycloak here, so please move over to a couple of my previous posts to see the detailed steps:

  • For information on implementing multi-tenancy in Keycloak and configuring authentication for a single tenant, refer to theImplementing multi-tenancy in Keycloak and authentication under Core
  • For information on enabling authorization mechanisms under Keycloak's tenants, see theImplementation of Authorization in Keycloak

Please follow the steps in the two articles above and configure as follows:

  1. Create a new file namedstickersRealm
  2. Switch tostickers Realm, create a new file namedpublicClient
  3. existpublic Enable Direct access grants under Client (temporarily enabled for testing purposes)
  4. Create a new file namedusergroupsIn this client scope, add a client scope of type Group Membership, set its Token Claim Name togroupsThen add this client scope to the Then add this client scope to thepublic Under Client
  5. existpublic Two new roles are created under Client:administratorcap (a poem)regular_user, and then create three new users:daxnetnobodysuperand set a password, then create a file calledpublicgroup (with the same name as the Client's name) in thepublic group, create a newusers group, and then in theusers group, create a newadministrators group.daxnetadd tousers group, which willsuperadd toadministrators group and set theusers group givenregular_usercharacters that willadministrators group givenadministratorcharacter
  6. existpublic Client's Authorization configuration creates four Scopes:admin.manage_usersrespond in singing; then create two RESOURCES:admin-apiIt has admin.manage_users scope, andstickers-apiIt hascap (a poem)The three scopes
  7. Under public Client, create two role-based Policies:require-admin-policyIt assigns theadministratorRole, andrequire-registered-user-policyIt assigns theregular_usercharacter
  8. Under Permissions, create four Permissions:
    1. admin-manage-users-permission: based on therequire-admin-policyThe role of theadmin.manage_users Scope
    2. Stickers-view-permission: based on therequire-registered-user-policyThe role of the Scope
    3. stickers-update-permission: based on therequire-registered-user-policyThe role of the Scope
    4. stickers-delete-permission: based on therequire-registered-user-policyThe role of the Scope

You can refer to the two articles listed above and these steps to configure Keycloak, or you can use the code in this chapter to compile the Keycloak Docker image directly and then run the container directly, after the Keycloak container is running, all the configurations are automatically imported, and at this point, you can use the settings according to the interface to learn than the steps above.

After completing the configuration on the Keycloak side, it's time to start modifying the project to make our API support authentication and authorization.

Enable authentication mechanisms in

About what is authentication and what is authorization, there will not be much discussion here, there are many related articles on the Internet, you can also get a detailed explanation and introduction through ChatGPT. We start by realizing the goal of allowing onlyregistered usercan access Stickers microservices, regardless of whether those users actually have access to some of the APIs in thescope of one's jurisdictionI have emphasized the concepts of "registered user" and "permission" in bold. I emphasize the "registered user" and "permission" two concepts in bold, you can distinguish what is authentication, what is authorized, in layman's terms: authentication is whether the user is allowed to use the site's services, authorization is allowed to use the site's services under the premise of whether the user can operate on some of the functions. Authorization is allowed to use the services of the site under the premise of whether the user can operate some of the functions.

Integrating authentication and authorization mechanisms in Core is very easy; first, add to the project the NuGet package, then in it, add the following code:

()
    .AddJwtBearer(options =>
{
     = "http://localhost:5600/realms/stickers";
     = false;
     = new 
    {
        NameClaimType = "preferred_username",
        RoleClaimType = ,
        ValidateIssuer = true,
        ValidateAudience = false
    };
});

The above code is used to initialize the authentication mechanism of Core, we use the authentication module of Jwt Bearer Token, in the configuration, specify the authentication authority Authority as the Base URL of the stickers Realm, and then configure the parameters of the token's authentication. HereNameClaimTypespecifies which Claim should be seen as the user name when parsing the access token, ditto for theRoleClaimTypespecifies which Claim should be seen as the user role. After starting the PostgreSQL and Keycloak containers, you can use a cURL command similar to the following to get the access token:

$ curl --location 'http://localhost:5600/realms/stickers/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=public' \
--data-urlencode 'client_secret=B2REunrXWN57KtQoJWoP2Dhr7gqKJrol' \
--data-urlencode 'username=daxnet' \
--data-urlencode 'password=daxnet'

Then open and copy this access token to the Encoded section of the Debugger, in the Decoded section you can see that the username is specified in the preferred_username field, which is the NameClaimType specified aspreferred_usernameThe reason:

Of course, the Middleware for Authentication and Authorization needs to be added to the file:

();
();

and enable the Authorize feature on the StickersController:

[ApiController]
[Authorize]
[Route("[controller]")]
public class StickersController(ISimplifiedDataAccessor dac) : ControllerBase
{
    // ...
}

If you start the Stickers API and use cURL to get all the "stickers", it will return 401 Unauthorized:

$ curl --location 'http://localhost:5141/stickers?asc=true&size=20&page=0' -v
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers?asc=true&size=20&page=0 HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 401 Unauthorized
< Content-Length: 0
< Date: Sat, 26 Oct 2024 13:05:21 GMT
< Server: Kestrel
< WWW-Authenticate: Bearer
< 
* Connection #0 to host localhost left intact

But if you add the access token you just got to the cURL command, you can access the API normally (the access token is too long, so I've truncated it here first):

$ curl --location 'http://localhost:5141/stickers?asc=true&size=20&page=0' \
       --header 'Authorization: Bearer eyJh...' -v
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers?asc=true&size=20&page=0 HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJh...
> 
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 26 Oct 2024 13:08:06 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"items":[],"pageIndex":0,"pageSize":20,"totalCount":0,"totalPages":0}

Enabling authorization mechanisms in

In my previous post on Keycloak-based Multi-tenant User Authorization Implementation under Core Web API" article, has described in detail how to complete the authorization based on Keycloak, in the Stickers case, I will use the same implementation, so here will not repeat the specific implementation process, only to introduce the Stickers microservices are unique to the part.

Above we have configured authorization in Keycloak, here is a general summary of authorization related configuration. First, we define four scopes, namely: admin.manage_users, , and . The so-called scopes are actuallyTypes of operations on resources; then, we define two kinds ofresource (such as manpower or tourism)Next, we define two Policies: require-admin-policy and require-registered-user-policy; we define two Policies: require-admin-policy and require-registered-user-policy; we define two Policies: require-admin-policy and require-registered-user-policy; and we define two Policies: require-admin-policy and require-registered-user-policy. Stickers-api represents the "stickers" API (the one provided by StickersController); next, we define two policies: require-admin-policy and require-registered-user-policy, which represent the "require admin role to do something" and the "require admin role to do something" respectively. and require-registered-user-policy, which mean "do something that requires the administrator role" and "do something that requires the registered user role" respectively. You can see that role-based authorization, in fact, in Keycloak's entire authorization system, is only one of the special cases, Keycloak supports the type of Policy, not only the role-based policy; Finally, the definition of the four Permission: admin-manage-users-permission, stickers-delete-permission, stickers-update-permission, and stickers-view-permission, each of which is associated with a corresponding policy (in this case, a role-based policy) and an operation type scope on the resource, which in turn is further controlled by the resource. These operation types are further referenced by the resource. So, in a nutshell, a Permission defines a resource that conforms to some kind ofbe tactful(Policy) of the visitor to a certainresource (such as manpower or tourism)(Resource) has the ability to accomplish whatType of operation(Scope) permissions.

Think about it and you'll realize that we don't really care what role the currently logged in user is in, we only care about that user'sdistinctionwhether or notAccess to a resourcefinalizecorresponding operationrequirements, and roles are just one of those traits. So, on the one hand, we define what resource the API is and what operations it supports, and on the other hand, when an authenticated user accesses the API, we read the name of the operation the user can perform on the resource from the user's Claims, and then compare the two, and whether the authenticated user meets the requirement of accessing the resource and performing the operation is already computed by the authorization module of Keycloak, and Keycloak just brings the result in the token it sends. Keycloak's authorization module has already done the calculation, Keycloak just sends the token with the calculation result.

The following figure shows the privilege evaluation for the user daxnet in Keycloak. From the evaluation result, we can see that the user has privileges on the stickers-api resource, as well as the operations; and has no privileges on the admin.manage_users on the admin-api resource. So, we just need to implement this judgment on.

Completing this judgment logic will roughly take two steps: first, using the access token, by placing thegrant_typeset tourn:ietf:params:oauth:grant-type:uma-ticketand call the/realms/stickers/protocol/openid-connect/tokeninterface to get user claims with authorization information, and then, when the API is accessed, look up from the user claims with authorization information according to the list of operations supported by the API to see if the operations supported by the API can be found in the user claims, and if they can be found, it means that the user can access the API, otherwise it returns the403 Forbidden

The full code will not be described in detail here, but it is still highly recommended to move to read the Keycloak-based Multi-tenant User Authorization Implementation under Core Web API" This blog post is accompanied by the source code for this chapter for details.

Here still involves the problem of user claims cache, because in the acquisition of user authorization information, there are two Keycloak calls, this is not particularly efficient, the follow-up will consider the introduction of caching mechanism to solve this problem.

After completing the implementation of the code, it is time to test it, using the daxnet user to get the access token:

$ curl --location 'http://localhost:5600/realms/stickers/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=public' \
--data-urlencode 'client_secret=B2REunrXWN57KtQoJWoP2Dhr7gqKJrol' \
--data-urlencode 'username=daxnet' \
--data-urlencode 'password=daxnet'

Then use this access token to access theGET /stickers API, as you can see, was able to return results successfully:

$ curl --location 'http://localhost:5141/stickers' \
     --header 'Authorization: Bearer eyJhbGci......' \
     -v && echo
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJhbGci......
> 
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Mon, 28 Oct 2024 13:15:58 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"items":[],"pageIndex":0,"pageSize":20,"totalCount":0,"totalPages":0}

Re-use the nobody user to get the access token:

$ curl --location 'http://localhost:5600/realms/stickers/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=public' \
--data-urlencode 'client_secret=B2REunrXWN57KtQoJWoP2Dhr7gqKJrol' \
--data-urlencode 'username=nobody' \
--data-urlencode 'password=nobody'

Then use this access token to access theGET /stickers API, you can see that the API returns403 Forbidden

$ curl --location 'http://localhost:5141/stickers' \
       --header 'Authorization: Bearer eyJhbGci......' -v && echo
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> GET /stickers HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJhbGci......
> 
< HTTP/1.1 403 Forbidden
< Content-Length: 0
< Date: Mon, 28 Oct 2024 13:18:31 GMT
< Server: Kestrel
< 
* Connection #0 to host localhost left intact

summarize

This article briefly describes the steps to implement authentication and authorization based on Keycloak on , due to some of the principle of the content and specific implementation details in my previous blog posts have been described in detail , so here is no longer repeated , it is recommended that you can read the code of this chapter in conjunction with these articles , I believe that there will be a lot of gains. The next chapter will be based on .NET Web Assembly to implement the front-end , and in the development environment to tune the entire front and back-end process.

source code (computing)

The source code for this chapter is in the branch chapter_4:/daxnet/stickers/tree/chapter_4/

Before downloading the source code, please delete the existingstickers-pgsql:devcap (a poem)stickers-keycloak:devtwo container images and remove thedocker_stickers_postgres_dataData Volume.

After downloading the source code, go to the docker directory, then compile and start the container:

$ docker compose -f  build
$ docker compose -f  up

You can now open the solution file directly with Visual Studio 2022 or JetBrains Rider and launch it for debugging runs.