Kong oAuth with a Django backend

Intro

In this tutorial we'll set up oauth with Kong.

Code for this tutorial can be found at: docker-kong-oauth.

Getting started, let's check out the example repo:

git clone https://github.com/toast38coza/docker-kong-oauth.git && cd docker-kong-oauth

This repo sets up a couple things to help us try out the oauth. We're going to use a simple Django app for our user backend cause it comes with some nice user management baked in.

For the purpose of simplicity, docker.local can be considered the url of the machine on which I'm running this with docker.

You'll want to replace docker.local with the appropriate IP (e.g.: 192.168.99.100)

Setting up our Kong environment

We're going to create two upstream APIs which we'll protect with oAuth. We'll create a client application, and finally: we'll integrate our userservice so that it can be used as the oauth backend.

In the repo, you'll find all of these apps, and you can get everything running with a simple:

docker-compose up -d 

Next, we'll add a service to Kong:

Note: If you've checked out the repo, you can use the bash scripts: register.sh and register2.sh to set this up quickly:

sh ./register.sh {host}

for example:

sh ./register.sh '192.168.99.100'

This will register both our services. It will spit out the json response. To add oauth. Now, take note of the id's and run:

sh ./register2.sh {service1.id} {service2.id}

for example.:

sh ./register2.sh 0d35c547-1311-4343-a567-7ca670d35637 7e9b3d3e-edc7-4c17-81d0-3f2eac91aaaf

Or, if you're doing it manually:

curl -X POST \
  --url http://docker.local:8001/apis/ \
  --data "name=service1" \
  --data "upstream_url=http://docker.local:8003" \
  --data "request_host=service1.com" \
  --data "request_path=/service1" \
  --data "strip_request_path=true" 

Response:

{"upstream_url":"http:\/\/docker.local:8003","strip_request_path":true,"request_path":"\/service1","id":"32427726-b3ea-43f8-9c95-b8b030327c28","created_at":1466282406000,"preserve_host":false,"name":"service1","request_host":"service1.com"}

Then we add the oauth plugin to this service:

curl -X POST http://docker.local:8001/apis/$1/plugins \
    --data "name=oauth2" 

Response

{"api_id":"32427726-b3ea-43f8-9c95-b8b030327c28","id":"9c461cba-2eab-4c6c-a85e-73df42a86345","created_at":1466282604000,"enabled":true,"name":"oauth2","config":{"mandatory_scope":false,"token_expiration":7200,"enable_implicit_grant":false,"hide_credentials":false,"provision_key":"3058d5b2d6c74fcba58f439951206c4b","accept_http_if_already_terminated":false,"enable_authorization_code":true,"enable_client_credentials":false,"enable_password_grant":false}}

Take note of the provision_key: 3058d5b2d6c74fcba58f439951206c4b.

You should now be able to verify that this is blocked with oauth:

curl http://docker.local:8000/service1

Response

{"error_description":"The access token is missing","error":"invalid_request"}

Ok. so now let's create out oauth backend.

Set up our user backend

It's important at this point for us to understand which actors exist in the system and what their responsibilities are:

The following actors are of importance:

  • The end-user
  • The client application
  • The oauth provider
  • The oauth protected service (referred to simple as service from here on)/

Imagine a news feed app. When you sign up, it allows you to authenticate with your Facebook account. From there, the app will make calls to the Facebook API on your behalf.

In this scenario:

  • You are the end-user
  • The news feed app is the client application
  • Facebook is the oauth provider
  • The Facebook API is the service

Hopefully that will clarify what we're up to a little ..

The oauth provider

In our project, the provider is the Django project userservice, and the functionality is in the app: userservice/oauth

Our oauth application will provide the following functionality:

  • /application/ - this is a place where client apps can register as an oauth application. End users will then give this application permission to act on their behalf.
  • /oauth - This is where oauth handshake takes place
  • /authorize - This is where our end user gives the client application permission to access the service on their behalf .. phew.

The whole process looks like this:

oauth process

Below we will create the various views necessary to facilitate the process of setting up our oauth system. For simplicity, the kong admin integration is abstracted to oauth/kong.py.

Create some users

Before we do any real coding, let's create some users. We'll use python manage.py createsuperuser to quickly create two users. Let's call the: joe-app-builder and joe-app-enduser. (normally our users would obviously not be superusers .. but for simplicity, let's go with this for now).

Create users

docker-compose run --rm userservice python manage.py createsuperuser
Username (leave blank to use '..'): joe-app-builder
...

docker-compose run --rm userservice python manage.py createsuperuser
Username (leave blank to use '..'): joe-app-enduser

Creating client applications

Userservice provides an interface where people can register client applications with the oauth backend.

Go to: http://docker.local:8002/application

You'll be asked to login. Login as joe-app-builder. Once logged in, you get the create application page.

What happens on this page:

Create a client application

This page does the following:

  • Create a Kong consumer for the currently logged in user (or get the matching consumer if it already exists)
#views.py:
consumer = kong.get_or_create_consumer(request.user)

This will essentially:

#username='joe-app-builder'
GET /consumers/:username
if 404: # doesnt exist:
  POST /consumer/ -d { "username": user.username, "custom_id": user.pk }

We can now create an application using the form on this page. Use:

Submitting the form will create a client application in Kong Admin. Essentially:

curl -X POST http://docker.local:8001/consumers/:consumer_id/oauth2 \
    --data "name=Test" \
    --data "redirect_uri=http://docker.local"  

Our oauth application is now registered:
Our oauth application

So now we have:

User: joe-app-builder who has a linked Kong Consumer, which in turn has a linked Kong Oauth2 Application.

You can query this information using Kong's admin API:

Consumer:

curl http://docker.local:8001/consumers/joe-app-builder

Consumer Application:

curl http://docker.local:8001/consumers/joe-app-builder/oauth2

Ok. So now we've got all the parts we need to start actually using oauth. Let's wire up some of the variables we need.

Edit: environment.env and update client_id and client_secret from above:

environment.env

client_id=860feefcb7144c51ae85e3b1c3347831
client_secret=df2abb32a6994f69bb001a7d76b59675
...

(if you forgot what those were .. you can get them with this request: curl http://docker.local:8001/consumers/joe-app-builder/oauth2).

Our client app will need these values in order to request oauth access and to generate the access_token from the access_code returned from the provider.

Our docker-compose setup uses the environment.env file to set environment variables for our client container.

Set provision_key in the provider

We also need to set the provision_key in the provider. You can get the provision_key by querying the plugins for the relevant upstream service. e.g.: http://docker.local:8001/apis/service1/plugins/

and update: userservice/userservice/settings.py:

OAUTH_SERVICE = {
    "host": "service1.com",
    "provision_key": "3058d5b2d6c74fcba58f439951206c4b"
}

Restart docker containers:

We need to restart our docker-containers for changes to take effect:

docker-compose stop client userservice
docker-compose up -d

Let's authenticate:

Back to the client app: docker.local:80

You'll see now that the client has our client_id and client_secret.

Client authentication page

The request to authorize is made to the userservice using by passing the client_id:

<a href="http://docker.local:8002/oauth/?client_id={{client_id}}"> 
  Authenticate
</a>

Before we click that link. Let's quickly make sure we log out of the userservice (we're currently logged in joe-app-builder).

Go to: http://docker.local:8002/admin and logout (top right).

Now click the Authenticate link. You'll be redirected to the userservice and asked to login. Login as joe-app-enduser this time.

After logging in, you will be redirected to a page asking you to authorize the application:

Authorization page

The two values in the text boxes are your UserService user_id, and your client_id (typically these would be hidden).

When you click authorize application you kick off the following process:

1. The User Service will request an access code on the client's behalf


url = "{}/oauth2/authorize" . format (settings.KONG_URL)
headers = { "Host": oauth_host }    
data = {
    "client_id": client_id,
    "response_type": "code",
    "provision_key": provision_key,
    "authenticated_userid": user_id
}
return requests.post(url, data, headers=headers)

2. Kong will respond with a redirect url:

.. and the userservice will perform the redirect.

{"redirect_uri":"http:\\/\\/docker.local?code=da97e283dff4490a9492d5dc0131a041"}

3. The client app will then swap the provided code for an access_token:

oauth_host = "service1.com"
headers = { "Host": oauth_host }    
data = {
    "grant_type": "authorization_code",
    "client_id": client_id,
    "client_secret": client_secret,
    "code": code,
}
url = "{}/oauth2/token" . format (kong_url)
return requests.post(url, data, headers=headers)

4. The client app can now make authenticated requests:

Finally, Kong will return our prized oauth token. We can then use that to make authenticated requests to upstream APIs:

url = "{}/service1?access_token={}" . format (kong_url, token.get('access_token'))
response = requests.get(url)

Authentication success

Conclusion

.. well that was easy .. :^/

So, I gotta admit .. this does look like a hellova lot of work. But ultimately, it's not actually that much code. The most complicated part to wrap your head around is understanding the oauth2 flow itself, and all the actors involved in the process. There's actually not a great deal of code involved.

Key Benefit: Avoid the need to sync data-stores:

Using Kong to handle your authentication is a no-brainer, and probably one of the biggest benefits to using Kong.

The lowest hanging fruit approach to achieve authentication with Kong is using something simple like key-auth. However, this quickly becomes overly complicated. You soon find yourself needing to keep two separate data stores in sync (e.g.: your backend user authentication store, and Kong's consumer store). It also somewhat confuses the line between end user authentication and client authentication.

With the oauth2 plugin these two are nicely segregated, and you can use your existing data authentication method without needing to keep it in sync with Kong.

References:

Postscript ..

  • TODO: package oauth so you can pip install django-kong-oauth2 ..

Questions answered

Q: If I have multiple updstream APIs, will I need to authenticate against each one of them?

A: No. The access_token which you use for service1 is still valid for service2

Notes:

Interestingly .. the reason that the provider returns the code, and not the token is because theoretically, the provider should not be able to make requests as the client. Interestingly however, all the information that would be required to get a token from a code (client_id and client_secret) is readily available via the Kong Admin API when armed only with client_id. This might be an oversite by the Kong team .. :^/