Using Kong to authorise requests and verify tokens
Introduction
Authentication, token validation, access control are typical cross functional requirements that don’t add any immediate value to services — offloading them to an API Gateway like Kong for on-premise or Kubernetes-based deployments allows the backend services to concentrate on business functionality and increases separation of concerns. In addition to that, Kong allows users to develop their own plugins in Lua or Go which enables all kinds of custom use cases to be handled in the API Gateway before a request even reaches the backend. In the following, Kong will be used to secure access to an NGINX web-server deployed in a Kubernetes cluster in combination with an existing JWT plugin to verify token correctness (in the following provided by Keycloak deployed into the same cluster) and a custom plugin that extracts the Keycloak roles from the JWT allowing the backend to get access to the roles without the need to parse the tokens at all.
Preconditions
The following has been tested in a Kubernetes cluster provided by Docker Desktop locally with an Ingress controller deployed making the cluster available under http://localhost and a Docker registry running under http://localhost:5000.
Phase 1: Keycloak and Kong (preparation)
Before we can secure Kong and make use of the JWT plugin we need to set up the source of the JWT: Keycloak. For this, we build a preliminary custom Kong image (for all referenced files see below “Appendix”) with
(cd kong-setup \
&& docker build . -t localhost:5000/kong-local \
&& docker push localhost:5000/kong-local)and deploy Keycloak and Kong into the cluster
kubectl apply \
-f kong.deployment.yaml \
-f kong.service.yaml \
-f kong.ingress.yaml \
-f keycloak.deployment.yaml \
-f keycloak.service.yamlNow we can access Keycloak under http://localhost/auth using the credentials set in keycloak.deployment.yaml and create a new client with “Client authentication” and “Authorization” enabled (taking note of the Client ID and Secret for later). Now to finish the Kong setup, we run
cat <<EOF | openssl x509 -pubkey -noout > key.pem
-----BEGIN CERTIFICATE-----
$(curl http://localhost/auth/realms/master/protocol/openid-connect/certs \
| jq -r '.keys[] \
| select(.alg == "RS256").x5c[0]')
-----END CERTIFICATE-----
EOFto extract Keycloak’s public key (which we need to verify that a JWT is signed by Keycloak as Keycloak is using the respective private key counterpart to sign the JWT) followed by
RSA_PUBLIC_KEY=$(awk '{printf "%s\\n", $0}' key.pem) \
envsubst < kong/kong.template.yaml > kong/kong.yamlto prepare the finalised Kong setup.
Phase 2: Kong and NGINX
Now that Keycloak is set up and Kong is configured we can compile the custom Kong plugin and build the final Kong image
(cd kong/keycloak-resource_access-roles-plugin \
&& env GOOS=linux GOARCH=amd64 go build .) \
&& (cd kong \
&& docker build . -t localhost:5000/kong-local \
&& docker push localhost:5000/kong-local)and both restart Kong and also install NGINX as our “backend”
kubectl delete po $(kubectl get pods -o custom-columns=PodName:.metadata.name,PodUID:.metadata.uid \
| grep kong \
| awk '{print $1}') \
&& kubectl apply -f nginx.deployment.yaml \
-f nginx.service.yamlPhase 3: Demo
As Keycloak, Kong and NGINX are now running to demonstrate how the set up is working we first connect to NGINX and run tcpdump to capture the incoming HTTP requests including all headers
kubectl exec $(kubectl get pods -o custom-columns=PodName:.metadata.name,PodUID:.metadata.uid \
| grep nginx \
| awk '{print $1}') \
-- bash -c "apt-get update; apt-get install tcpdump -y; tcpdump -A 'tcp port 80'"Then, without an access token perform a request to Kong
curl -v http://localhost/apiwhich is expected to fail with a HTTP 401 (Unauthorized) and the response {“message”:”Unauthorized”}. So we obtain a valid access token via
CLIENT_ID=... CLIENT_SECRET=... URL=http://localhost/auth/realms/master/protocol/openid-connect/token \
ACCESS_TOKEN=$(curl -d "grant_type=client_credentials&client_id="$CLIENT_ID"&client_secret="$CLIENT_SECRET $URL | jq -r ."access_token")(use Client ID and Secret from earlier) followed by
curl -v -H "Authorization: Bearer "$ACCESS_TOKEN http://localhost/apiGet Johannes Geiger’s stories in your inbox
Join Medium for free to get updates from this writer.
and the console window running tcpdump should display the incoming HTTP header Keycloak-Resource-Access-Client-Roles: uma_protection as the default role. To further verify that the Go plugin is working as intended we can create a new role for our Client in Keycloak and assign it via Service account roles and on requesting a new access token and repeating the authenticated request above we should see the new role logged as well.
Summary
Running Kong as an API Gateway in front of other services in Kubernetes is a great way to separate concerns and reduce cognitive load from the backend services. Instead of every service having to verify tokens, extract roles from tokens or perform other cross-functional tasks like adding Correlation ID, logging certain information or masking data on their own, Kong can be leveraged and the ability to easily add custom plugins written in Lua or Go make it even more flexible.
Appendix
kong-setup/Dockerfile:
FROM kong/kong-gateway:latest
COPY kong.yaml /
RUN cp /etc/kong/kong.conf.default /etc/kong/kong.confkong-setup/kong.yaml:
_format_version: "1.1"
services:
- name: keycloak
url: http://keycloak
routes:
- name: keycloak
paths:
- /authkong.deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: kong
labels:
app: kong
spec:
replicas: 1
selector:
matchLabels:
app: kong
template:
metadata:
labels:
app: kong
spec:
containers:
- name: kong
image: localhost:5000/kong-local
env:
- name: KONG_DATABASE
value: "off"
- name: KONG_DECLARATIVE_CONFIG
value: "kong.yaml"
ports:
- containerPort: 8000kong.service.yaml:
apiVersion: v1
kind: Service
metadata:
name: kong
spec:
selector:
app: kong
ports:
- protocol: TCP
port: 80
targetPort: 8000kong.ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kong
spec:
ingressClassName: nginx
rules:
- host: localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kong
port:
number: 80keycloak.deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
labels:
app: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak
env:
- name: KC_HOSTNAME
value: http://localhost/auth
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
value: admin
ports:
- containerPort: 8080
args:
- start-devkeycloak.service.yaml:
apiVersion: v1
kind: Service
metadata:
name: keycloak
spec:
selector:
app: keycloak
ports:
- protocol: TCP
port: 80
targetPort: 8080kong/keycloak-resource_access-roles-plugin/go.mod:
module keycloak-resource_access-roles-plugin
go 1.22.1
require (
github.com/Kong/go-pdk v0.11.0
github.com/golang-jwt/jwt v3.2.2+incompatible
)
require (
github.com/ugorji/go/codec v1.2.12 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)kong/keycloak-resource_access-roles-plugin/main.go:
package main
import (
"github.com/Kong/go-pdk"
"github.com/Kong/go-pdk/server"
"github.com/golang-jwt/jwt"
"log"
)
type Configuration struct {
HeaderName string `json:"header_name"`
}
func New() interface{} {
return &Configuration{}
}
const Version = "1.0.0"
const Priority = 1
func getToken(kong *pdk.PDK) (*jwt.Token, error) {
header, err := kong.Request.GetHeader(`Authorization`)
if err != nil {
return nil, err
}
tokenString := header[len(`Bearer `):]
parser := jwt.Parser{
ValidMethods: nil,
UseJSONNumber: false,
SkipClaimsValidation: true,
}
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, err
}
return token, nil
}
func (conf *Configuration) clearHeader(kong *pdk.PDK) error {
err := kong.ServiceRequest.ClearHeader(conf.HeaderName)
if err != nil {
return err
}
return nil
}
func (conf *Configuration) setHeaders(kong *pdk.PDK, claims jwt.MapClaims) error {
clientId := claims[`client_id`].(string)
resourceAccess := claims[`resource_access`].(map[string]interface{})
resourceAccessClient := resourceAccess[clientId].(map[string]interface{})
resourceAccessClientRoles := resourceAccessClient[`roles`].([]interface{})
for _, role := range resourceAccessClientRoles {
err := kong.ServiceRequest.AddHeader(conf.HeaderName, role.(string))
if err != nil {
return err
}
}
return nil
}
func (conf *Configuration) Access(kong *pdk.PDK) {
token, err := getToken(kong)
if err != nil {
log.Println(err)
kong.Response.ExitStatus(500)
}
err = conf.clearHeader(kong)
if err != nil {
log.Println(err)
kong.Response.ExitStatus(500)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
err = conf.setHeaders(kong, claims)
if err != nil {
log.Println(err)
kong.Response.ExitStatus(500)
}
}
}
func main() {
err := server.StartServer(New, Version, Priority)
if err != nil {
panic(err)
}
}kong/Dockerfile:
FROM kong/kong-gateway:latest
COPY keycloak-resource_access-roles-plugin/keycloak-resource_access-roles-plugin /usr/local/kong/keycloak-resource_access-roles-plugin
ENV KONG_PLUGINS="bundled,keycloak-resource_access-roles-plugin"
ENV KONG_PLUGINSERVER_NAMES="keycloak-resource_access-roles-plugin"
ENV KONG_PLUGINSERVER_KEYCLOAK_RESOURCE_ACCESS_ROLES_PLUGIN_START_CMD="/usr/local/kong/keycloak-resource_access-roles-plugin"
ENV KONG_PLUGINSERVER_KEYCLOAK_RESOURCE_ACCESS_ROLES_PLUGIN_QUERY_CMD="/usr/local/kong/keycloak-resource_access-roles-plugin -dump"
COPY kong.yaml /
RUN cp /etc/kong/kong.conf.default /etc/kong/kong.confkong/kong.template.yaml:
_format_version: "1.1"
consumers:
- username: api
jwt_secrets:
- consumer: api
key: http://localhost/auth/realms/master
algorithm: RS256
rsa_public_key: "$RSA_PUBLIC_KEY"
services:
- name: keycloak
url: http://keycloak
routes:
- name: keycloak
paths:
- /auth
strip_path: true
- name: api
url: http://api
routes:
- name: api
paths:
- /api
plugins:
- name: jwt
service: api
enabled: true
config:
key_claim_name: iss
claims_to_verify:
- exp
- name: keycloak-resource_access-roles-plugin
service: api
enabled: true
config:
header_name: Keycloak-Resource-Access-Client-Rolesnginx.deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
protocol: TCPnginx.service.yaml:
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80








