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>=5.2python>=3.10the 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”
Set the Client type to OpenID Connect and choose a meaningful Client ID. The other options do not matter for this tutorial.
On the second page, enable Client authentication and the Standard Flow (also named Authorization Code Flow which is the one that we want).
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 URLandHome URLredirects to the root of our application http://app.local:8082With
Valid redirect URIswe allow the user to be redirected to our application, or the one listening onlocalhost:9091and127.0.0.1:9091(for debug purposes)With
Valid post logout redirect URIsthe user can be redirected to our application after logout: http://app.local:8082/*Web originsis 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
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.
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:
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:
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:
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 viewnamed<op_name>-login, here handled on the/auth/loginpatha
logout viewnamed<op_name>-logout, here handled on the/auth/logoutpatha
callback viewnamed<op_name>-callback, here handled on the/auth/callbackpatha
backchannel logout viewnamed<op_name>-backchannel-logout, here handled on the/auth/backchannel-logoutpath
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.
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:
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:
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
Configuring drf spectacular (swagger) integration
After settings drf-spectacular with swagger integration, you can setup the swagger authentication module to use OIDC.
Note
To our knowledge, redoc is no supported.
Usually we have two OIDC clients for a django API :
one is the client used to check incoming token (the drf client in the tutorial above)
one is a full client used to authenticate users on the admin page
You can configure swagger to use your full client to generate OIDC tokens. The setup is quite easy, you just need to tell swagger about your client credentials.
In your settings.py add :
SPECTACULAR_SETTINGS = {
"SWAGGER_UI_OAUTH2_CONFIG": {
"clientId": os.getenv("SSO_CLIENT_ID"), # adapt this to your projet
"clientSecret": os.getenv("SSO_CLIENT_SECRET"), # adapt this to your project
"scopes": [], # optional
},
}
You will also need to add a special view to register the token in your browser after you’ve authenticated
succesfully. In the following example, you should have some equivalent for api/schema and api/docs.
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerOauthRedirectView,
SpectacularSwaggerView,
)
urlpatterns += [
# The following 2 urls need to be configured according to drf-spectacular documentation
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"
),
# This is the view that you need to add
path(
"api/docs/oauth2-redirect.html", # FIX this so that this view is next to the SpectacularSwaggerView
CustomSpectacularView.as_view(),
name="swagger-ui-oauth",
)
]
You should now have an authorize button on the the top right corner of your swagger page.
After clicking on it, a popup opens. Scroll top the top. You should reach a section named “openIdConnect (OAuth2, authorization_code) “. The client_id and client_secret shoud already be filled from your env variables. Scroll down until you see an authorize button. Click on it.
After clicking on authorize, you are redirected to to your SSO login page. After login you are redirected to the special swagger page which stores the token to be used on the main swagger page.
You’re done ! 🎉
Integrating django-pyoidc with the admin site
To use this library to authenticate users into the admin site, you must first configure a provider.
We will assume that you configured a provider name ‘sso’ for the remaining of this tutorial.
To integrate OIDC with the admin site :
you need to have some views implementing OIDC behaviour and logging users into django authentication backend (this is provided by this library)
you need to change the admin login page to redirect to the OIDC login view : this is the purpose of this tutorial
To change the admin login page you have two options :
declare a template that overrides the login page of the admin
plug a custom template into the admin site
We will go for the second option because we think that it is the cleanest. If the admin template organization was to change, our login page will still be used.
This is the login page that will be implemented in this tutorial :
Creating a custom admin site
Create a sites.py file somewhere suitable. We tend to put it next to our settings module as we find that this configuration is more ‘project related’ than ‘application related’.
from django.contrib.admin import AdminSite
class OIDCLoginAdminSite(AdminSite):
login_template = "my_project/login.html" # we will declare this template later
site_header = "Administration Site"
site = OIDCLoginAdminSite(name="my-project-admin")
# We need a callable for the next step
def get_site():
return site
Next, we need to register this admin site with the admin config. Create an admin_config.py module somewhere suitable (next to the settings for example) :
from django.contrib.admin.apps import AdminConfig
class CustomAdmin(AdminConfig):
# Change my_project with the name of the package holding your sites.py module
default_site = "my_project.sites.get_site"
Now you have the following files next to each other :
settings.py
sites.py
admin_config.py
To use the custom login page, you must import the admin app from your custom config instead of django.contrib.auth in your INSTALLED_APPS setting :
INSTALLED_APPS = [
# The following lines replaces 'django.contrib.auth'
"my_project.admin_config.CustomAdmin",
]
And you are done !