Auth0 with Kong JWT Plugin in docker  - simple guide with python scripts
Tags: auth0, docker, python, kong
Published: 2020-11-06 14:38:59

__Kong__ and __Auth0__ are very powerful platforms. 
Of course, the more you can do with a tool, the more documentation you need to read, and sometimes it could be frustrating 😬

So, I've created this guide to show how you can easily configure everything on your local environment and play around in any way you like.

To keep this article short, I've assumed that you already know a bit about Docker, Kong and Auth0. In any case here are some links:
- [Use the Docker command line](https://docs.docker.com/engine/reference/commandline/cli/)
- [Kong JWT Plugin](https://docs.konghq.com/hub/kong-inc/jwt/)
- [Obtaining an Access Token example](https://auth0.com/docs/quickstart/backend/aspnet-core-webapi/02-using)

Also, you'll need three things to follow this guide:
- API you can use for testing;
- Auth0 account/tenant where you can generate access token;
- IDE to run python scripts (I use PyCharm);

I use python scripts as an example of how you can automate configuration process. That might be a great help for those who need to configure Kong for a running system or a different environments.

## First things first!

Let's start with a Kong running in a docker container.

We need to create a Docker network and database for Kong:

```python
docker network create kong-net
```

```python
docker run -d --name kong-database \
               --network=kong-net \
               -p 5432:5432 \
               -e "POSTGRES_USER=kong" \
               -e "POSTGRES_DB=kong" \
               -e "POSTGRES_PASSWORD=kong" \
               postgres:9.6
```

Database migration: 

```python
docker run --rm \
     --network=kong-net \
     -e "KONG_DATABASE=postgres" \
     -e "KONG_PG_HOST=kong-database" \
     -e "KONG_PG_USER=kong" \
     -e "KONG_PG_PASSWORD=kong" \
     -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
     kong:latest kong migrations bootstrap
```

And now, we can run our container:

```python
docker run -d --name kong \
     --network=kong-net \
     -e "KONG_DATABASE=postgres" \
     -e "KONG_PG_HOST=kong-database" \
     -e "KONG_PG_USER=kong" \
     -e "KONG_PG_PASSWORD=kong" \
     -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
     -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
     -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
     -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
     -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
     -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
     -p 8000:8000 \
     -p 8443:8443 \
     -p 127.0.0.1:8001:8001 \
     -p 127.0.0.1:8444:8444 \
     kong:latest
```

Try to make a test: 

```python
curl -i http://localhost:8001/
```

If you have some troubles, check if your containers are running:

```python 
docker ps
```

If you don't see `kong` and `kong-database` containers in a list, then you'll need to start them explicitly:

```python
docker start kong kong-database 
```

You can also use Postman for testing:

![Screenshot 2020-11-05 at 00.41.06](https://dev-to-uploads.s3.amazonaws.com/i/5vzq5t3w7g9y9wf93uai.png) 


## Time for some scripts!

_Btw, using python is not mandatory. If you don't like it, you still can follow those steps with any other language or use curl commands._

__Lets first define a configuration object for our new service and route.__


```python
# you'll need those imports later
import requests
import json

host = "192.168.1.98"

config = {
    "service": {
        "name": "test-service",
        "url": "http://" + host + ":7400/api/test/data",
    },
    "route": {
        "protocols": ["http"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "hosts": [host],
        "paths": ["/(data)/*"],
        "headers": None,
    },
}
```

Here `service.url` is a place where Kong will redirect us if we make a call to `localhost:8000/data` with a header `Host 192.168.1.98` (theoretically 🙃).

If you don't want to use local API, then you can change `service.url` and `route.hosts` to use your values. Otherwise, I bet, you might have a question: __"Why 192.168.1.98 and not localhost?"__. Hold that thought, I'll get back to it.

__Next step - creating Service and Route in Kong.__

First of all, copy this script to the same file where you have your config object and take a close look on what's going on there. Don't run it yet - we still need to check one more thing.

```python
# create a service using our config object
print(config["service"]["name"])
serviceUrl = "http://localhost:8001/services"

serviceResponse = requests.post(url=serviceUrl, json=config["service"])

# print service id to ensure that it was successfully created 
serviceId = (json.loads(serviceResponse.content))["id"]
print("serviceId ", serviceId)

# create a route using our config object
routeUrl = "http://localhost:8001/routes"

config["route"]["service"] = {
    "id": serviceId
}

routeResponse = requests.post(url=routeUrl, json=config["route"])
routeId = (json.loads(routeResponse.content))["id"]

# print route id because you'll need it to add a plugin later
print("routeId", routeId)
```
 
__And now - why do we use '192.168.1.98' and not localhost for our service.__

I've been playing around, trying to proxy my local service with Kong running in a docker container. When I was calling my local API through Kong, it kept returning 502 error with a message `An invalid response was received from the upstream server`.

The reason is because Kong was pointed to `localhost` of my docker container. Obviously that didn't work because my service wasn't hosted there.

_Of course, you don't need to care about it if you're using external API for testing._

Easy solution here is to register your Kong service with your local host machine’s IP address (e.g. 192.168.1.98). Btw, it will be different for your machine. Probably you might want to check it. Run `/sbin/ifconfig` for mac or `ipconfig` for Windows machine.

You should be able to see smth like that:

![Screenshot 2020-11-05 at 15.51.44](https://dev-to-uploads.s3.amazonaws.com/i/tplwsm2cysgs1koknjfy.png)

Don't forget to update `host` in python config with your IP and keep in mind, that it should be different than the one Kong uses.

Ok, and now you can try to run your script. If you don't see any errors in IDE output, then you can try to receive a list of services or routes (Just to make sure they're really created).

![Screenshot 2020-11-05 at 00.41.52](https://dev-to-uploads.s3.amazonaws.com/i/56uoqa9z43odfofhf4yf.png) 
 
__Now, if you're lucky enough, you should be able to access your API directly or through the Kong.__
_Note: your API should be up and running._

Direct request:

![Screenshot 2020-11-05 at 00.42.33](https://dev-to-uploads.s3.amazonaws.com/i/jl7ulwkwfzvdvnu5wx0u.png) 

Request through Kong:

![Screenshot 2020-11-05 at 23.18.59](https://dev-to-uploads.s3.amazonaws.com/i/12ii2irqko4uk37a8fjc.png) 
 
  
## Last step. Let's make it secure!

I already had a public key for my Auth0 account, but if you don't have it, then execute commands below and save a file with a public key in the same directory with your python script.

_Note: don't forget to replace those values `{COMPANYNAME}.{REGION-ID}`_

You'll need to download your Auth0 account’s X509 Certificate:

```python
$ curl -o {COMPANYNAME}.pem https://{COMPANYNAME}.{REGION-ID}.auth0.com/pem
```
And extract the public key from the X509 Certificate:

```python
$ openssl x509 -pubkey -noout -in {COMPANYNAME}.pem > pubkey.pem
```

__Great! Now we need to add JWT Plugin and consumer__

If you forgot to save your routeId, then you can request it from `http://localhost:8001/routes` (find your route if you have more than one) and copy its id from the response.

```python
# create a plugin for our route
# here you'll need to insert your routeId
pluginUrl = "http://localhost:8001/routes/" + {ROUTEID} + "/plugins"

pluginData = {
    "name": "jwt"
}

requests.post(url=pluginUrl, json=pluginData)
```

_Note: script won't work, if you forgot to put your public key in the same folder._

```python
# create a global consumer
consumerUrl = "http://localhost:8001/consumers"
consumerData = {
    "username": "test-consumer",
    "custom_id": "test-consumer-id",
}

consumerResponse = requests.post(url=consumerUrl, json=consumerData)

# print consumer id to ensure that it was successfully created
consumerId = (json.loads(consumerResponse.content))["id"]
print("consumerId ", consumerId)

# Add JWT plugin with Auth0 public key
basicUrl = consumerUrl + "/" + consumerId + "/jwt"
basicData = {
    "algorithm": "RS256",
    "key": "https://{COMPANYNAME}.{REGION-ID}.auth0.com/",
    "rsa_public_key": open('pubkey.pem', 'rb')
}

requests.post(url=basicUrl, files=basicData)
```

## Vualá 🥳

Now you won't be be able to call your API through Kong without bearer token. Also, only tokens signed by Auth0 will work.

You can try to make the same call `localhost:8000/data` and, if you did everything right, you should receive __401 Unauthorized__.
Header `Host 192.168.1.98` should be attached to the request when you're calling Kong.

![Screenshot 2020-11-05 at 00.44.41](https://dev-to-uploads.s3.amazonaws.com/i/ntlx91tmlp87yk3i1lmo.png) 

When bearer token is attached, your request should be accepted: 

![Screenshot 2020-11-05 at 00.45.21](https://dev-to-uploads.s3.amazonaws.com/i/c0fepytff0wcw8i0cg5b.png)
 

## Few more things

1. If you need to verify scopes or claims in your token and you're using Kong Enterprise version, you might want to have a look at a JWT Signer Plugin. This plugin will verify and (re-)sign token for you.

2. If you have, for example, SPA application which is supposed to access multiple APIs, you can check this topic from Auht0 documentation [Represent Multiple APIs Using a Single Logical API](https://auth0.com/docs/authorization/represent-multiple-apis-using-a-single-logical-api).

3. If you have Kong and all your APIs are protected with it and trusted, you don't have to generate M2M tokens. Of course, if you need it or find it valuable, then sure, go for it. It's just something you should think about, will it really make your system more secure or it will just bring unnecessary expenses?

## Thank you!

I hope this guide was helpful. Thank you for reading. Don't hesitate to contact me, ask questions or let me know if you found a mistake 🤗