Securing APIs with OpenID Connect (Authentication) — Part 1

Configure Kong API Gateway with the OIDC Plugin and Keycloak to secure APIs.

INTRODUCTION

  • Kong — An open-source API gateway and platform that acts as middleware between compute clients and API-centric applications.
  • Keycloak — An open-source software product to allow single sign-on with Identity and Access Management implementing standard specifications such as OIDC, OAuth 2.0, and SAML.

We will be using Docker Compose to manage our containers. We will also be using a self-signed certificate to communicate over HTTPS. You can refer to this article to generate self-signed certificates. You can also use a CA-signed certificate if you have one already or you can also check this article to know more on how to generate a CA-signed certificate.

ASSUMPTIONS

  • Docker Compose is installed.
  • You are familiar with Spring Boot.
  • Self-signed/CA-signed certificates are created.

Now let’s get started! Wait, before we do that you may want to grab a cup of coffee and get comfortable because this may seem a lot to get digested at once

OVERVIEW

We will explore this diagram in the last post where we will see how the applications communicate with each other and the order in which they do it to implement OIDC. For now, let’s concentrate on creating a user-demo app and spinning a docker container for it. First, pick a base directory and create two child directories where we’ll store the code for our user-demo app and infrastructure(kong and keycloak):

$ mkdir /opt/docker
$ cd /opt/docker
$ mkdir cert # For certificates
$ mkdir microservices # For user-demo app setup
$ mkdir infrastructure # For Kong and Keycloak setup
$ cd infrastructure
$ mkdir -p data/persist/postgre # For PostgreSQL persistence volume
$ mkdir -p data/persist/mariadb # For MariaDB persistance volume

SETTING UP THE USER-DEMO APP

$ cd /opt/docker/microservices
$ git clone git@github.com:amkuio/user-demo.git
$ cd user-demo

To create a user-demo container and communicate over HTTPS we need two things now. First the user-demo.jar and second an SSL certificate. So first, let’s build the user-demo app to generate user-demo.jar. We can use this jar file to spin a container for user-demo microservice.

$ mvn clean install

We have generated the user-demo.jar file successfully and the file is present under user-demo/target directory. Let’s copy this file to a separate directory:

$ mkdir -p user/data
$ cp user-demo/target/user-demo.jar user/data/

Now let’s generate a self-signed certificate by using keystore command.

$ mkdir -p user/config/ssl
$ cd user/config/ssl
$ keytool -genkey -alias sscerts -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650
Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]:
What is the name of your organizational unit?
[Unknown]:
What is the name of your organization?
[Unknown]:
What is the name of your City or Locality?
[Unknown]:
What is the name of your State or Province?
[Unknown]:
What is the two-letter country code for this unit?
[Unknown]:
Is CN = Unknown, OU=Unknown, O = Unknown, L = Unknown, ST = Unknown, C = Unknown correct?
[no]: yes

We can create an application.properties file to override the application.properties inside the container and configure the generated keystore related information, to enable the microservice to get accessed over HTTPS.

$ cd /opt/docker/microservices/user/config
$ touch application.properties

Let’s copy the below contents to the application.properties

# The format used for the keystore. It could be set to JKS in case it is a JKS file
server.ssl.key-store-type=PKCS12
# The path to the keystore containing the certificate
server.ssl.key-store=/usr/src/user/config/ssl/keystore.p12
# The password used to generate the certificate
server.ssl.key-store-password=password
# The alias mapped to the certificate
server.ssl.key-alias=sscerts

Next, create a docker-compose.yml

$ touch docker-compose.yml

Open this file with your favorite text editor and add the following:

version: '3.7'
services:
user:
image: openjdk:11-jdk
ports:
- 8081:8080
volumes:
- ./user/data/binaries:/usr/src/user/data
- ./user/data/config:/usr/src/user/config
working_dir: /usr/src/user
command: "java -jar ./data/user-demo.jar"

We are exposing our user-demo microservice on port 8100. Now, let’s spin up the user service with the following command:

$ docker-compose up -d

Verify the service is running (make sure state is Up):

$ docker-compose ps

We can also try to navigate the below URL in the browser to see if we are able to access the user-demo service over HTTPS:

https://localhost:8080/rest/api/user

SETTING UP THE KONG API GATEWAY

To integrate the OpenID Connect plugin with Kong API Gateway we will have to create a customized Dockerfile that will install kong-oidc plugin. Let’s create a Dockerfile:

$ cd /opt/docker/infrastructure
$ touch Dockerfile

Open the Dockerfile with your favorite text editor and populate it with the following:

FROM kong:2.0.0-alpine
LABEL description="Docker image containing Alpine + Kong 2.0.0 + kong-oidc plugin"
USER root
RUN apk update && apk add git unzip luarocks
RUN luarocks install kong-oidc
USER kong

Let’s build the image:

$ docker build -t kong:2.0.0-alpine-oidc .

This will generate a new image kong:2.0.0-alpine-oidc with kong-oidc plugin installed in it.

Configure Kong

Create a docker-compose.yml file:

$ touch docker-compose.yml

Open the file with your favorite text editor and populate it with the following contents:

NOTE — Before we do that let’s make sure that the self-signed/CA-signed certificates are already created and present under /opt/docker/cert directory.

version: '3.4'

networks:
kong-net:

services:
kong-db:
image: postgres:9.6
volumes:
- ./data/persist/postgre:/var/lib/postgresql/data
networks:
- kong-net
ports:
- "5432:5432"
environment:
POSTGRES_DB: kong
POSTGRES_USER: kong
POSTGRES_PASSWORD: kong

kong-migration:
image: kong:2.0.0-alpine-oidc
entrypoint: ["/bin/sh","-c"]
command:
- |
kong migrations bootstrap
restart: on-failure
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-db
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong
links:
- kong-db
depends_on:
- kong-db
networks:
- kong-net
healthcheck:
test: ["CMD", "pg_isready", "-U", "kong"]
interval: 5s
timeout: 5s
retries: 5

kong:
image: kong:2.0.0-alpine-oidc
depends_on:
- kong-migration
- kong-db
networks:
- kong-net
ports:
- "58000:8000" # Listener
- "58001:8001" # Admin API
- "58443:8443" # Listener (SSL)
- "58444:8444" # Admin API (SSL)
environment:
KONG_LUA_SSL_TRUSTED_CERTIFICATE: /etc/kong/certnew.cer
KONG_SSL_CERT: /etc/kong/certnew.cer
KONG_SSL_CERT_KEY: /etc/kong/server.key
KONG_ADMIN_SSL_CERT: /etc/kong/kong_admin/certnew.cer
KONG_ADMIN_SSL_CERT_KEY: /etc/kong/kong_admin/server.key
KONG_NGINX_PROXY_SSL_CLIENT_CERTIFICATE: /etc/kong/certnew.cer
KONG_NGINX_ADMIN_SSL_CLIENT_CERTIFICATE: /etc/kong/certnew.cer
KONG_CLIENT_SSL_CERT: /etc/kong/certnew.cer
KONG_CLIENT_SSL_CERT_KEY: /etc/kong/server.key
KONG_DATABASE: postgres
KONG_PG_HOST: kong-db
KONG_PG_PORT: 5432
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong
KONG_PG_DATABASE: kong
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_PROXY_LISTEN: 0.0.0.0:8000, 0.0.0.0:8443 ssl
KONG_ADMIN_LISTEN: 0.0.0.0:8001, 0.0.0.0:8444 ssl
KONG_PLUGINS: oidc
volumes:
- /opt/docker/cert/certnew.cer:/etc/kong/certnew.cer:ro,Z
- /opt/docker/cert/server.key:/etc/kong/server.key:ro,Z
- /opt/docker/cert/certnew.cer:/etc/kong/kong_admin/certnew.cer:ro,Z
- /opt/docker/cert/server.key:/etc/kong/kong_admin/server.key:ro,Z

We are exposing Kong on ports 58444 and 58443 for admin and proxy ports respectively. This means that to configure Kong we are supposed to use port 58444 and to access our configured services we should be able to access it on 58443.

Let’s spin the services one by one. First, spin the kong-db service with the following command.

$ docker-compose up -d kong-db

Verify that the service is running (make sure the state is “Up”):

$ docker-compose ps

Now we will run the migrations on the kong-db service. The following command will spin up a kong service which will run the command kong migrations bootstrap.

$ docker-compose up -d kong-migration

Verify that the service status.

$ docker-compose ps

Notice that the status of the kong-migration service is Exit 0. The idea is that the kong-migration service runs migration command on kong-db service and exits. We don’t want this service running all the time. Running migration is a one-time job.

Finally, we can bring up kong service:

$ docker-compose up -d kong

Verify that the service is running and is healthy (make sure state is “Up”):

$ docker-compose ps

Now we can try hitting the Kong admin API on a web browser and see if the odic plugin is available or not:

https://localhost:58444/

Let’s look for plugins section to confirm:

{
"plugins":{
"enabled_in_cluster":[

],
"available_on_server":{
"oidc":true
}
}
...
...
}

Next, let’s add the services and routes necessary to hit our user-demo API from Kong:

$ curl -k -X POST https://localhost:58444/services \
--data name=user-service \
--data url=https://localhost:8081/rest/api/user

{
"host":"localhost",
"created_at":1614256203,
"connect_timeout":60000,
"id":"6ee6830f-d65a-4c80-9973-b12400bea10d",
"protocol":"https",
"name":"user-service",
"read_timeout":60000,
"port":8081,
"path":"\/rest\/api\/user",
"updated_at":1614256203,
"retries":5,
"write_timeout":60000,
"tags":null,
"client_certificate":null
}

Let’s add a route against user-service which we created above:

curl -k -X POST https://localhost:58444/routes \
--data service.name=user-service \
--data paths[]=/user-demo-service

{
"id":"3c61f869-6576-485d-a257-9e1a9cea7a1a",
"path_handling":"v0",
"paths":[
"\/user-demo-service"
],
"destinations":null,
"headers":null,
"protocols":[
"http",
"https"
],
"methods":null,
"snis":null,
"service":{
"id":"6ee6830f-d65a-4c80-9973-b12400bea10d"
},
"name":null,
"strip_path":true,
"preserve_host":false,
"regex_priority":0,
"updated_at":1614256226,
"sources":null,
"hosts":null,
"https_redirect_status_code":426,
"tags":null,
"created_at":1614256226
}

Finally, let’s verify we’ve set everything up correctly. To do that we can hit the below URL to the web browser and should expect a similar result when we tried accessing the user-demo API directly.

https://localhost:58443/user-demo-service

SETTING UP KEYCLOAK

Configure Keycloak

Open the docker-compose.yml and add a network for Keycloak:

networks: 
kong-net:
keycloak-net:

Next, add the keycloak and keycloak database to services in the docker-compose.yml:

services:
...
keycloak-db:
image: mariadb
volumes:
- ./data/persist/mariadb:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: keycloak
MYSQL_USER: keycloak
MYSQL_PASSWORD: password

keycloak:
image: quay.io/keycloak/keycloak:latest
ports:
- 8100:8443
volumes:
- /opt/docker/cert:/etc/x509/https
environment:
DB_VENDOR: mariadb
DB_ADDR: keycloak-db
DB_DATABASE: keycloak
DB_USER: keycloak
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Admin@123
depends_on:
- keycloak-db

We are exposing Keycloak on port 8100.

Notice for the Keycloak database I am using MariaDB. You can use PostgreSQL if you want. I have just used MariaDB for a variation, that’s all.

Finally, let’s spin the keycloak-db and keycloak service:

$ docker-compose up -d keycloak-db keycloak

Verify that both the services are running with status “Up”:

$ docker-compose ps

Finally, we can navigate to https://localhost:8100/. You should see the Keycloak admin console page:

Let’s try to login to Keycloak by clicking on to Administration Console. On the next screen, you will be asked to enter username and password. You can user admin:Admin@123 and log in.

This is the default screen that you will see on successful login. You may notice that by default we are into the master realm. You may add a new realm by clicking on the arrow next to master on the top right corner and then Add Realm. But we are going to use master realm here.

After we login to Keycloak, we need to do a couple of things. First add a client, which will represent Kong. And second, add a user, which we will use to login and access services that Kong is protecting.

Add a client to Keycloak

To add a client, click the Clients link the sidebar, and then the Create button on the right side of the Clients page:

On the Add Client page, fill in the Client ID as kong and click the Save button.

On the details page, set the Access Type to Confidential, turn On the Service Accounts Grant Enabled, the Root URL to https://localhost:58443 (https://{kong_hostname}:{proxy_port}), the Validate Redirect URIs to /user-demo-service/* and click the Save button.

Once you save, a new tab Credentials will appear on the details page. Click on that and record the Secret. We will need it later.

Add a user to Keycloak

To add a user, click the Users tab on the left sidebar, then click the Add user button on the right side of the window.

On the next page, set the Username to test. Then click the Save button.

Click on the Credentials tab and enter in a New Password, Password Confirmation, and make sure the Temporary switch is set to OFF. Then click the Set Password button.

SETUP KONG TO WORK WITH KEYCLOAK

In order to configure Kong so that it can talk to Keycloak and authenticate test user, we will use the OIDC plugin. If you remember we have already installed OIDC to Kong in the Set up Kong API Gateway section. We just need to create an OIDC plugin entry Kong database, just like we did for adding our service and route. The Kong OIDC plugin needs three things to hook up with Keycloak: the Client ID, the client secret, and the discovery endpoint. The discovery endpoint is what the Kong OIDC plugin can hit in order to get information on where it can do authentication, token introspection, etc.

You can navigate to the below link and locate the introspection service URL for later usage:

https://localhost:8100/auth/realms/master/.well-known/openid-configuration

Before we add an OIDC plugin, we need to have the Keycloak ${HOST_IP} know to us. This is because if you try to access /.well-known/openid-configuration URL above inside Kong container, it may not be able to access as it is a URL exposed by Keycloak, and both Kong and Keycloak are running in different containers. Therefore, we can identify the Keycloak host IP using ipconfig in Windows and ifconfig in the Linux environment. ${CLIENT_SECRET} is have already recorded in Add Client section.

$ curl -k -X POST https://localhost:58444/plugins \
--data name=oidc \
--data config.realm=master \
--data config.client_id=kong \
--data config.client_secret=${CLIENT_SECRET} \
--data config.discovery=https://${HOST_IP}:8100/auth/realms/master/.well-known/openid-configuration

{
"created_at":1614279897,
"config":{
"response_type":"code",
"introspection_endpoint":null,
"filters":null,
"bearer_only":"no",
"ssl_verify":"no",
"session_secret":null,
"introspection_endpoint_auth_method":null,
"realm":"master",
"redirect_after_logout_uri":"\/",
"scope":"openid",
"token_endpoint_auth_method":"client_secret_post",
"logout_path":"\/logout",
"client_id":"kong",
"client_secret":"990a70a9-541e-4fc9-b3ca-8ac5f474629d",
"discovery":"https:\/\/${HOST_IP}:8100\/auth\/realms\/master\/.well-known\/openid-configuration",
"recovery_page_path":null,
"redirect_uri_path":null
},
"id":"e559ed39-31c1-4405-96bb-94eed634cd12",
"service":null,
"enabled":true,
"protocols":[
"grpc",
"grpcs",
"http",
"https"
],
"name":"oidc",
"consumer":null,
"route":null,
"tags":null
}

That’s it. Now when you try to navigate https://localhost:58443/user-demo-service/, Kong should redirect you to the Keycloak login page. Log in using the user we created in Add user section (test: password), you should be redirected to the original content you requested.

CONCLUSION