This is a small Django project to demonstrate using private keys and OpenID Connect
You'll build a Django application with a login backed by OpenID Connect
It may be helpful to set up a virtualenv to isolate the setup for this project. (There are several tutorials on how to use it, so I'm not going to document that here)
The following is a shortened form of the offical Django tutorial.
(python_3.5)$ pip install Django
Django 'sites' can be thought of as a project that contains one or more appliations under them.
(python_3.5)$ django-admin startproject mysite
This will create the following:
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py
We'll create our 'app' under the site.
(python_3.5)$ cd mysite
(python_3.5) mysite$ python manage.py startapp simpleapp
This will create a subdirectory called simpleapp containing your new app
It's structure will look like:
simpleapp/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
We'll do three things to create our index view for our application.
- Modify
simpleapp/views.py - Create a template at
simpleapp/templates/simpleapp.html - "Install" the app into our Django site
Django created the file for us, but it only contains an import. Change it to look like:
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request, "simpleapp.html")This will simply forward to the template we're about to create
In this simple case our template is just going to be an HTML file since our application isn't really doing anything. You can read up on using Django templates here
For now just create the simpleapp/templates directory and add in simpleapp.html so it looks like:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>simpleapp</title>
</head>
<body>
<p>Hello, world. You're at the simpleapp index.</p>
</body>
</html>While this isn't needed for our super simple application at this point, it will be later on.
We have to define which URL patterns go to which views. Add in a simpleapp/urls.py file that looks like:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
]In order for Django to know about the template, we have to 'install' the application.
To do this we'll add it to the mysite/mysite/settings.py file.
Open that file, and find the INSTALLED_APPS variable. Add in our simpleapp like so:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'simpleapp'
]Now we need to tell our project about the simpleapp URLs.
Modify the mysite/urls.py file to look like:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^simpleapp/', include('simpleapp.urls')),
url(r'^admin/', admin.site.urls),
]Now we can test out our application.
Run the following from the top level mysite directory where manage.py lives:
(python_3.5) mysite$ python manage.py runserver
Once that is done you should be able to visit your new application at http://localhost:8000/simpleapp
In this section we'll
- Install and setup
django-oidc - Create a protected portion of our web application
django-oidc is a module for Django that allows authentication to happen via OpenID Connect.
The problem with it is there are many forks of the project that have different feature sets.
The fork located at https://github.com/koriaf/django-oidc seems to be the best
version for working with Python 3 and Django 1.11. It has also been updated with a modifcation to support private_key_jwt
based client authentication.
As a note, django-oidc is basically a wrapper around the https://github.com/OpenIDC/pyoidc
project that is a generic python module for using OpenID connect.
To install the version of django-oidc we want we'll use pip:
(python_3.5) mysite$ pip install git+https://github.com/koriaf/django-oidc.git
To configure our project we'll have to
- Install
django-oidcinmysite/settings.py - Add the urls to
mysite/urls.py - Define our OP and Client
- Set up our private keys for client authentication
We'll 'install' the module the same way we installed our simpleapp in mysite/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'djangooidc',
'simpleapp'
]NOTE: The application does not contain the '-' when you install it, just add djangooidc to the INSTALLED_APPS
Thankfully django-oidc has already defined all the appropriate call back URLs, etc. We just have to tell our project
how to use them. This is done in the mysite/urls.py file.
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^simpleapp/', include('simpleapp.urls')),
url(r'^openid/', include('djangooidc.urls')),
url(r'^admin/', admin.site.urls),
]All the configuration for using OIDC will be in the mysite/settings.py file
First we add in the AUTHENTICATION BACKENDS, this isn't in the default settings.py so we'll put it after the DATABASES
setup. We need the default ModelBackend as well as our OpenIdConnectBackend.
We'll also set up the LOGIN_URL while we're here. By default this can just be /openid but that will bring you to a
login that allows you to select which OpenID provider, or figure it out from typing in an email, etc. in a form that can be customize
Since we're going to only set up a single OpenID Connect provider, and only want to use that one though,
we can use the pattern of /openid/openid/<providername>, where <providername> is defined when we configure the
provider, mitreid in this particular case.
# DEFINE AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'djangooidc.backends.OpenIdConnectBackend'
]
# Set LOGIN_URL (for django oidc)
LOGIN_URL = '/openid/openid/mitreid'Next we'll want to set up the PyOIDC settings
The version provided here has several options commented out, so we'll only focus on the core ones here.
Add all of these to the mysite/settings.py file
# We're disabling dynamic client registration.
OIDC_ALLOW_DYNAMIC_OP = False
# Default OIDC behavoir will be the 'code' workflow
OIDC_DEFAULT_BEHAVIOUR = {
"response_type": "code",
"scope": ["openid", "profile", "email", "address", "phone"],
}
# Set up our providers. Here the name is 'mitreid' which we use in the login URL above
OIDC_PROVIDERS = {
"mitreid": {
"srv_discovery_url": "https://mitreid.org/",
"behaviour": OIDC_DEFAULT_BEHAVIOUR,
"client_registration": {
"client_id": "f5458edf-5163-4b3b-a965-577922719fb1",
"redirect_uris": ["http://localhost:8000/openid/callback/login/"],
'token_endpoint_auth_method': ['private_key_jwt'],
"enc_kid": "rsa_test",
"keyset_jwk_file": "file://keys/keyset.jwk"
}
}
}The important bit here is the Providers setup. We are defining only one provider (mitreid), and we are not using
dynamic client registration. We have a sev_discovery_url that allows pyoidc to find all the endpoints for us, look
in the comments to see how to manually specify those if needed.
We then use client_registration to define our client. We have the client_id which matches the client id on your
OIDC server. The redirect_uris should contain only one, which is defined by django-oidc as http://<server>:<port>/openid/callback/login.
The next three values, token_endpoint_auth_method, enc_kid, and keyset_jwk_file are all used to set up using
private keys for client authentication rather than a client_secret.
token_endpoint_auth_method is pretty self explaintory, we're using private_key_jwt.
enc_kid must match the kid in the JWK file for the key pair used when setting up the client on your OIDC provider.
(Here we need both the private and public key, where the provider should only have the public.)
keyset_jwk_file specifies the keyset file that should be used. (Where the kid lives.)
We'll place the encryption keys in /mysite/keys. We only need a Key Set, which should look like:
{
"keys": [
{
"kty": "RSA",
"d": "CzY14i8NxPUAPmH9JHR5VIMezv0WOunBB0NkfZmzUGrJSn5DXGrRIs0psERyHLSBKVTpRGcp9ZlcDfMZV81e-v1a_sz9IogCNd15y4UUcpFKuAKAY0s4Fa8whu3u7iL0Zut_tKlBxKPhAtgX3Urc6neRURFvhfzD4zrOaKRbZwf446JxrqyyDSQfGUBhTkiURsvvch0GojaUS-hzuI8tRzgowC5K8jHrl8Bg__ai7iuNfHOFxH83oAlSM4fEt-Fi4FLpev2dxXhvL8sJOVN7CReDsxYWR7l1rzlzH_cER6uA2QX9xCYyqMCegdCfTEEaCGKr28LssRBiSCe6DylRgQ",
"e": "AQAB",
"use": "sig",
"kid": "rsa_test",
"alg": "RS256",
"n": "lTmpgjt5cyV-0v0QWRdiarUZRd6U5muDRrqHOe1UwA6lZUD68LlfvwcYnR8cInMZd3o1Tmx4cvePP8zOCEBnlVVeAamxXaRT59w2iZyXyw90u9or-R3qAMtK-eObJH29jMjRog06U-TXzBExkRcyz8c3JIlI9t1eNMESsBQsrglwGFTa_PFqLM0sGEtuCs9L-Q9ca0-9rlounVhGJMKF4BNEbNoBLeoK-fcwsx45IKo-iId_vJTrK_lTGXy4VwQnR4uHzHWOtvx9h2PVdsaZcSgHk4aIyvN8B5SB2h6DVR1_QtBLwcbY5D-JNT1fMpQqGmYmVHW-AteO82YMpIaIjw"
}
]
}I've also included the Key pair and just the public key versions of the jwk in this directory for convenience.
DO NOT USE THESE KEYS - Generate your own. You can use a site like: https://mkjwk.org/ with a
key length of 2048, key use of signing, and your own kid that you can specify in the client definition above.
Now that we've set up django-oidc it's pretty simple to lock down parts of our web application.
At this point we'll want to migrate our project to set up the database. By default Django uses sqlite3, but doesn't
recommend that for production. For our little test here, it is just fine though.
Run the following to set up the user tables, etc.:
(python_3.5) mysite$ python manage.py migrate
Now we'll change our simple app template to show if we've logged in or not, and provide a link to a resource that only
authenticated users can get to. Due to the default processors that Django has, we'll have a user object passed on to the
template automatically.
Modify simpleapp/templates/simpleapp.html so it looks like:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% if user.is_authenticated %}
<p>Welcome, {{ user.get_username }}. Thanks for logging in.</p>
{% else %}
<p>Welcome, new user. Please log in.</p>
<a href="{% url 'openid_with_op_name' op_name='mitreid' %}?next={{request.path}}">Log In</a>
{% endif %}
<br>
<p>Here is a link to a <a href="{% url 'protected' %}">protected resource</a></p>
</body>
</html>If you look you'll see we have an if statement that determines if the user is logged in. If not, we
provide a login link. Because of the way authentication works on Django, we have to give it a next= parameter
so it knows where to go. {{request.path}} resolves to the current path. The openid_with_op_name url will create
a link to login with a specific OIDC provider (in this case our mitreid one).
If we are logged in, we're just displaying the user.get_username value in a welcome message.
Next we'll define the protected view that the link at the bottom goes to.
First we'll modify simpleapp/views.py to create the view. add in the following imports and function.
from django.contrib.auth.decorators import login_required
...
@login_required
def protected(request):
return render(request, "protected.html")That's it. The @login_required indicates that you can only get to this view if you are logged in.
Now we'll create the template as simpleapp/templates/protected.html.
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<p><a href="{% url 'index' %}">Return</a></p>
<p>You should only see this if you are logged in.</p>
<table>
<tr><td>user.get_username</td><td>{{user.get_username}}</td></tr>
<tr><td>user.email</td><td>{{user.email}}</td></tr>
<tr><td>user.first_name</td><td>{{user.first_name}}</td></tr>
<tr><td>user.last_name</td><td>{{user.last_name}}</td></tr>
<tr><td>user.username</td><td>{{user.last_login}}</td></tr>
</table>
</body>
</html>And lastly we'll add protected to our urls. Change the simpleapp/urls.py to the following:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^protected/$', views.protected, name='protected')
]Fire up the application now with:
(python_3.5) mysite$ python manage.py runserver
Now when you go to http://localhost:8000/simpleapp/ you should see that you are not
logged in. You can either click the login link, or just click right on the protected link, and you should be re-directed
to the mitreid.org OIDC login page. Once logged in, you should come back to this page or see the protected page with
user information.
NOTE: going to openid/logout will attempt to end the session at the OIDC provider. This is not recommended as not
all providers support this, plus it will log the user out of other applications.
You'll find the completed version of this tutorial in the completed sub directory.