Search code examples
reactjsauthenticationkeycloakopenid-connectkong

ReactJS proper configuration of OIDC on Kong + Keycloak


I am wondering what should be the proper configuration when I have the ReactJS front end application and I want to give access to BE only to authorized and authenticated users through OIDC.

I have the following setup:

Browser ----> Kong ----> /admin* (ReactJS FE)
                   ----> /api* (BE REST API)
               |
            Keycloak

The goal here is that the Kong will do the authorization and authentication so it can be externalized from the application.

Kong is using OIDC plugin that can handle authorization when the user would like to open application in browser and is not authorized. It will redirect to Keycloak, and once user is authorized, it will come back to Kong for further processing of JWT token.

This will happen when user hits for example https://localhost:8000/admin. So far, so good.

Now, the actual functionality is provided by the /api* end points. Therefore I need to get the token or cookie and be able to validate the token to have access to the API (here we are authenticating user).

What is the proper way how to configure this? Should be the API also protected by OIDC with the same flow, or it should only validate JWT.

The first option, when the /api* has the OIDC plugin enabled:

  • open https://localhost:8000/admin with unauthorized user
  • Kong will detect it and use the OIDC plugin to authorize through Keycloak
  • browser is redirected to Keycloak, user will provide credentials
  • browser will send info about authorization back to Kong, and it will finish authorization and get the JWT token
  • from now on, the ReactJS FE will use cookies and eventually JWT token to access the https://localhost:8000/api* from the ReactJS application

Now, when the access is expired or revoked, and the ReactJS application is still opened, I get errors, because the OIDC plugin will try to redirect on /api* calls with 302, however I am still in the ReactJS app, and it cannot handle it. The only way here is to refresh the complete ReactJS app and it will do the authorization again and it will work.

Here, I am utilizing kong-oidc plugin.

The second option I am thinking about, when the /api* route does not use OIDC plugin, but only validates JWT tokens:

  • open https://localhost:8000/admin with unauthorized user
  • Kong will detect it and use the OIDC plugin to authorize through Keycloak
  • browser is redirected to Keycloak, user will provide credentials
  • browser will send info about authorization back to Kong, and it will finish authorization and get the JWT token
  • from now on, the ReactJS FE will use cookies and eventually JWT token to access the https://localhost:8000/api* from the ReactJS application

When the session is expired or revoked, the ReactJS application will start getting the 401 unauthorized instead of 302. In this case, the ReactJS should know that it should start the OIDC again to get new token, or refresh.

In this second options, I can use kong-oidc plugin for OIDC on the ReactJS, and kong-plugin-jwt-keycloak plugin for validation of tokens on the API level.

I currently have the following configuration of Kong, which should be behaving according the first option:

_format_version: '2.1'
_transform: true
services:
  - name: core
    host: core-service
    port: 8080
    protocol: http
    routes:
      - name: core_route-oidc
        strip_path: false
        paths:
          - /api*
        plugins:
          - name: oidc
            config:
              client_secret: s0qKH5qItTWoxpBt7Zrj348ZhZ7woAbk
              client_id: kong
              bearer_only: 'no'
              realm: Kong
              introspection_endpoint: >-
                https://keycloak.local/realms/Kong/protocol/openid-connect/token/introspect
              discovery: >-
                https://keycloak.local/realms/Kong/.well-known/openid-configuration
          - name: request-transformer
            config:
              remove:
                headers:
                  - cookie
  - name: fe-administrator
    host: fe-administrator-service
    port: 80
    protocol: http
    path: /
    routes:
      - name: fe-administrator_route-oidc
        strip_path: false
        paths:
          - /admin*
        plugins:
          - name: oidc
            config:
              client_secret: s0qKH5qItTWoxpBt7Zrj348ZhZ7woAbk
              client_id: kong
              bearer_only: 'no'
              realm: Kong
              introspection_endpoint: >-
                https://keycloak.local/realms/Kong/protocol/openid-connect/token/introspect
              discovery: >-
                https://keycloak.local/realms/Kong/.well-known/openid-configuration
          - name: request-transformer
            config:
              remove:
                headers:
                  - cookie
plugins:
  - name: cors
    config:
      origins:
        - '*'
      credentials: true
      max_age: 3600
      exposed_headers:
        - X-Auth-Token
      preflight_continue: false

But I am wondering if this is proper configuration. Also, if some client application would like to access only /api*, without accessing the ReactJS front end application, it will not use the same OIDC flow, do I understand it correctly?

Should be the /api* configured according second option and provides validation of JWT tokens only? Something like the following Kong config:

_format_version: '2.1'
_transform: true
services:
  - name: core
    host: core-service
    port: 8080
    protocol: http
    routes:
      - name: core_route-oidc
        strip_path: false
        paths:
          - /api*
        plugins:
          - name: jwt-keycloak
            config:
              allowed_iss: >-
                https://keycloak.local/realms/Kong
          - name: request-transformer
            config:
              remove:
                headers:
                  - cookie
  - name: fe-administrator
    host: fe-administrator-service
    port: 80
    protocol: http
    path: /
    routes:
      - name: fe-administrator_route-oidc
        strip_path: false
        paths:
          - /admin*
        plugins:
          - name: oidc
            config:
              client_secret: s0qKH5qItTWoxpBt7Zrj348ZhZ7woAbk
              client_id: kong
              bearer_only: 'no'
              realm: Kong
              introspection_endpoint: >-
                https://keycloak.local/realms/Kong/protocol/openid-connect/token/introspect
              discovery: >-
                https://keycloak.local/realms/Kong/.well-known/openid-configuration
          - name: request-transformer
            config:
              remove:
                headers:
                  - cookie
plugins:
  - name: cors
    config:
      origins:
        - '*'
      credentials: true
      max_age: 3600
      exposed_headers:
        - X-Auth-Token
      preflight_continue: false

And the ReactJS application must properly handle 401 unauthorized.

I would like to understand how this should be done properly.


Solution

  • Implementing an SPA secured by cookies is tricky. It is worth comparing against the behaviour of a backend for frontend, the second of the below roles, and basing your solution on that.

    URLs

    COOKIE API ROUTE

    The second URL is called during Ajax requests from the SPA. There should be no OIDC or redirects here, just cookie / token validation and forwarding.

    This route should translate cookies to tokens, apply CSRF checks, then forward the token to the API. If the cookie / token is invalid or expired a 401 should be returned. The keycloak plugin you mention expects to receive JWTs, not cookies.

    BACKEND FOR FRONTEND

    A BFF would run on the second URL and provide endpoints tailored to what the React app needs, such as these:

    It can be difficult to implement a BFF as an API gateway plugin, so sometimes it is instead implemented as a utility API, which runs behind the gateway.