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:
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:
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:
- Name: Test
- Redirect URL: http://docker.local
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:
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
.
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:
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)
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 canpip 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 .. :^/