Auth0 with Kong JWT Plugin in docker - simple guide with python scripts

borzenko_lena

Olena Borzenko

Posted on November 6, 2020

Auth0 with Kong JWT Plugin in docker  - simple guide with python scripts

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:

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:

docker network create kong-net
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Database migration:

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
Enter fullscreen mode Exit fullscreen mode

And now, we can run our container:

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
Enter fullscreen mode Exit fullscreen mode

Try to make a test:

curl -i http://localhost:8001/
Enter fullscreen mode Exit fullscreen mode

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

docker ps
Enter fullscreen mode Exit fullscreen mode

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

docker start kong kong-database 
Enter fullscreen mode Exit fullscreen mode

You can also use Postman for testing:

Screenshot 2020-11-05 at 00.41.06

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.

# 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,
    },
}
Enter fullscreen mode Exit fullscreen mode

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.

# 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)
Enter fullscreen mode Exit fullscreen mode

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

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

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

Request through Kong:

Screenshot 2020-11-05 at 23.18.59

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:

$ curl -o {COMPANYNAME}.pem https://{COMPANYNAME}.{REGION-ID}.auth0.com/pem
Enter fullscreen mode Exit fullscreen mode

And extract the public key from the X509 Certificate:

$ openssl x509 -pubkey -noout -in {COMPANYNAME}.pem > pubkey.pem
Enter fullscreen mode Exit fullscreen mode

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.

# 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)
Enter fullscreen mode Exit fullscreen mode

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

# 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)
Enter fullscreen mode Exit fullscreen mode

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

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

Screenshot 2020-11-05 at 00.45.21

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.

  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 🤗

💖 💪 🙅 🚩
borzenko_lena
Olena Borzenko

Posted on November 6, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related