Search code examples
authenticationjwtnestjskeycloakservice-accounts

Keycloak: Authorization between services and the public frontend


I have an application which consists of a frontend and several backend services. The authentication is done via Keycloak. The workflow looks like this: The user logs into the frontend and gets a token from Keycloak. This token is sent to the backend with every request.

The following image explains the current architecture:

enter image description here

In Keycloak I have the following clients:

1. Frontend

  • Access Type: public
  • Client Protocol: openid-connect

2. Core Service

  • Access Type: bearer-only
  • Client Protocol: openid-connect

3. User Service

  • Access Type: bearer-only
  • Client Protocol: openid-connect

How can I validate calls between services now?

I would imagine something like a service account and these have the possibility to call each other independently from the bearer-token from the frontend. The problem is that both services can be called from the frontend as well as between each other.

Edit:

My API is written with NestJS.

The API of the user-service: enter image description here

And this is how I call the user-service in my core-service: enter image description here

and this is my keycloak configuration for the the user-service: enter image description here

At the moment I don't add anything to the request and I don't have any extra configuration on the interface. So I added the @Resource('user-service')-Annotation to the Controller and the @Scope()-Annotation to the Endpoint.

After that I don't get an error immediately and the endpoint is called.I can log that the logic is executed. But as response I still get a 401 Unauthorized Error.

Do I need to specify a scope or what do I need to add in the @Resource-Annotation?

Edit 2:

I'll try to show you my current situation with many screenshots.

Initial situation

enter image description here

Here is your drawing again. For me, points 1-5 work and point 8 works even if I do not call another service.

My Configuration

That this works, I have the following configuration:

Just Frontend and Core Service

Frontend: enter image description here

Core-Service: enter image description here

For the core service (gutachten-backend), I do not need to make any further configurations for this. I also have 2 different roles and I can specify them within the API.

Using Postman I send a request to the API and get the token from http://KEYCLOAK-SERVER_URL/auth/realms/REALM_NAME/protocol/openid-connect/token.

enter image description here

These are my 2 testing methods. I call the first one and it works. The following is logged. Means the token is validated received and I get Access: enter image description here

Calling the user service

Now I call the second method. This method calls the user-service.

This is my request in the core-service: enter image description here I do not add anything else to my request. Like a bearer token in the header.

The endpoint in the user service is just a test method which logs a message.

This is my configuration for the user service: enter image description here

I have now tried something with resources, policies and permissions.

Resource

enter image description here

Policies

Role-Policy enter image description here

Client-Policy: enter image description here

Permission

enter image description here

And analogously the client permission

Questions and thoughts

  • All steps from the first drawing seem to work except 6 and 7
  • Do I need to add more information to my request from core service to user service?
  • How to deal with root url and resource urls?
  • In the code in the API, do I need to additionally configure the endpoints and specify the specific resources and policies? (NestJS offers the possibility to provide controllers with a @Resource('<name>') and endpoints with @Scopes([<list>])) Additionally, through a tutorial on setting up keyacloak in NestJS, I turned on the following config:

This adds a global level resource guard, which is permissive. Only controllers annotated with @Resource and methods with @Scopes are handled by this guard.


Solution

  • For people who have the same problem in the future. The answer from @BenchVue helped a lot to understand the concept in general.

    What was missing is that a token must also be added for each request between services. Namely the token of the client.

    So before the request is sent, the following query takes place. This is the method to get the token for a client:

    getAccessToken(): Observable<string> {
        const header = {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
        };
        return this.httpService.post(
          '{{keycloakurl}}/auth/realms/{{realm}}/protocol/openid-connect/token',
          `grant_type=client_credentials&client_id={{clientId}}&client_secret={{clientSecret}}`,
          header).pipe(
            map((response) => {
              return response.data.access_token as string;
            }
            ));
      }