Connecting a django project to an identity provider

In this tutorial, you will learn how to use this library along with an identity provider (IP) to setup a single-sign-on system. For the purpose of this tutorial, we will use Keycloak as an IP.

Requirements

To use this library, make sure that you meet the following requirements:

  • django>=4.2

  • python>=3.8

  • the Session middleware is enabled

  • a cache backend for django (redis, etc.)

  • an OIDC-compliant identity provider

Installation

To install this library the easiest way is to use the pypi package

pip install django-pyoidc

Configuring your SSO

Next, you should configure a client in your identity provider configuration interface.

Warning

Incorrect configuration of your Identity Provider can create security issues. Please make sure you understand the values you input and their impact on the security level of your system.

We provide instructions for Keycloak (version 18 and more), a free and open source Identity Provider maintained by Red Hat.

Keycloak

Start by connecting as your realm admin on the administration interface.

We will create a new client which supports the ‘Authorization Code Flow’. Go to the client list of your realm and click on “Create client”

Screenshot of the client list from a Keycloak instance

Set the Client type to OpenID Connect and choose a meaningful Client ID. The other options do not matter for this tutorial.

Screenshot of the first page of a client configuration form from Keycloak

On the second page, enable Client authentication and the Standard Flow (also named Authorization Code Flow which is the one that we want).

Screenshot of the second page of a client configuration form from Keycloak

Click on save and your client should be visible in the client list.

You can now configure your URLs. In the following example, the Django application is hosted at app.local:8082.

We configure our client URLs as such:

  • Root URL and Home URL redirects to the root of our application http://app.local:8082

  • With Valid redirect URIs we allow the user to be redirected to our application, or the one listening on localhost:9091 and 127.0.0.1:9091 (for debug purposes)

  • With Valid post logout redirect URIs the user can be redirected to our application after logout: http://app.local:8082/*

  • Web origins is set to + which allows (through CORS) all origins from the redirect URIs

TODO: using a 2nd app at localhost:9091 is confusing, remove that, use a localhost:something, better

Screenshot of url configuration page for Keycloak client

Take note of your Client ID and visit the Credentials Page to find your Client Secret. You will need both to configure the OIDC connector.

Screenshot of the Credentials page from a test client

Finally, click on Realm Settings in the left menu, and scroll down to the Endpoints section. Copy the OpenID Endpoint Configuration URL as you will need it later (this is the autodiscovery URL).

Congratulation, your Keycloak configuration is complete! 🎉

Other Identity provider

Configuring your Django project

Install the application

It is now time to configure your Django project.

First, add the library app (django_pyoidc) to your django applications, after django.contrib.sessions and django.contrib.auth:

settings.py
INSTALLED_APPS = [
    "django.contrib.auth",
    "django.contrib.sessions",
    ...
    "django_pyoidc"
]

Warning

Do not forget later to run the migrations! This module requires some extra database storage tables.

Configure a cache backend

You must have a cache backend for this library to work! The OIDC protocol is very stateful and we use Django cache system to store data. If you want to understand why, you can read the Cache management page.

For the sake of this tutorial, you can use this cache management snippet (it should be pasted in your settings.py):

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    }
}

Warning

Do not use those settings in production! Go read the django documentation for more details.

Configure the library

First, make sure that the Session middleware is enabled.

We will use django_pyoidc provider system to generate the library configuration and views.

When using provider, you must provide 4 settings:

  • the provider class to use

  • the OIDC client ID: this is your identifier on the IP side (this is not a user account, this must be a client in the OIDC terminology)

  • the OIDC client secret: this is your secret on the IP side

  • the OIDC discovery URL: this url allows us to discover the various endpoint of the identity provider, easing the configuration

You must also define a provider name that will be used with other classes from this library. In the following example, we define a provider named sso which uses Keycloak18Provider and fetches it’s credential from two environment variables:

settings.py
DJANGO_PYOIDC = {
    # This is the name that your identity provider will have within the library
    "sso": {
        # change the following line to use your provider
        "provider_class": "django_pyoidc.providers.keycloak_18.Keycloak18Provider",

        # your secret should not be stored in settings.py, load them from an env variable
        "client_secret": os.getenv("SSO_CLIENT_SECRET"),
        "client_id": os.getenv("SSO_CLIENT_ID"),

        # Your autodiscovery url should go here
        "provider_discovery_uri": "https://keycloak.example.com/auth/realms/fixme",

        # This setting allow the library to cache the provider configuration auto-detected using
        # the `provider_discovery_uri` setting
        "oidc_cache_provider_metadata": True,
    },

When you need to configure a setting for your identity provider, it means that you have to update the dictionary in this setting. For example, if you were to configure oidc_paths_prefix for your Keycloak provider, you would add oidc_paths_prefix : <your value> to the sso dictionary.

Please note that drf is a reserved provider name (see Configuring django_rest_framework for more details)

Copy-paste this snippet to your settings.py. Make sure to modify provider_discovery_uri.

Generate the URLs

We provide a facility that generates all the views needed for a provider. This is implemented by the OIDCHelper class. This class reads the DJANGO_PYOIDC setting and uses it’s configuration to generate views.

To use it, you must instantiate it with op_name=<the name of your identity provider>.

Here is how to do it for our tutorial:

urls.py
from django_pyoidc.helper import OIDCHelper

# `op_name` must be the name of your identity provider as used in the `DJANGO_PYOIDC` setting
oidc_helper = OIDCHelper(op_name="sso")

urlpatterns = [
    path(
        "auth/",
        include((oidc_helper.get_urlpatterns(), "django_pyoidc"), namespace="auth"),
    ),
]

This will create 4 views in your URL configuration. They all have a name that derives from the op_name that you used to create your provider.

  • a login view named <op_name>-login, here handled on the /auth/login path

  • a logout view named <op_name>-logout, here handled on the /auth/logout path

  • a callback view named <op_name>-callback, here handled on the /auth/callback path

  • a backchannel logout view named <op_name>-backchannel-logout, here handled on the /auth/backchannel-logout path

Tip

You can override the naming behavior by configuring the setting oidc_paths_prefix of your identity provider. The view names would then be <oidc_paths_prefix>_<view_name>.

You should now be able to use the view names from this library to redirect the user to a login/logout page.

Configuring django_rest_framework

When using OIDC to authenticate an API, things are a little bit different than on a full stack website:

  • we do not want to redirect users on login pages, or to manage logout

  • we are receiving OIDC Bearer tokens – access tokens– (generated from other clients of the SSO) and the task is mainly to check that this token is valid and extract the user from it.

To configure django_rest_framework, you must create a special provider named drf. The configuration is similar to the one made in Configure the library.

settings.py
DJANGO_PYOIDC = {
    # This is the name that your identity provider will have within the library
    "drf": {
        "provider_class": "django_pyoidc.providers.keycloak_18.Keycloak18Provider",
        "client_secret": os.getenv("SSO_CLIENT_SECRET"),
        "client_id": os.getenv("SSO_CLIENT_ID"),
        "provider_discovery_uri": os.getenv(
            "SSO_ENDPOINT", "https://keycloak.example.com/auth/realms/fixme"
        ),
        "oidc_cache_provider_metadata": True,
    },

Note

Usually your application should request a different client_id for the apimode (like a my-app-full client_id for a confidential classical OIDC client and a my-app-api client_id for a bearer-only OIDC client in Keycloak). But if you have only one client_id it’s OK to simply make a copy for the settings.

Once you declared those settings, you can configure DEFAULT_AUTHENTICATION_CLASSES to use django_pyoidc.drf.authentication.OIDCBearerAuthentication to use this authentication class on all your views:

settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "django_pyoidc.drf.authentication.OIDCBearerAuthentication"
    ]
}

You can also set this class on a per-view basis using the authentication_classes attribute:

views.py
from django_pyoidc.drf.authentication import OIDCBearerAuthentication

class ExampleViewSet(ModelViewSet):
    authentication_classes = [OIDCBearerAuthentication]

This class looks up the OIDC provider named drf in the DJANGO_PYOIDC setting. As such, you can only have one provider for all your API authentication, as you can not define two drf keys in the settings.

Tip

Look up the drf documentation for more details about authentication classes.

Tip

Taking a look at the documentation of use_introspection_on_access_tokens might be a good idea if you run into some issues