Installation¶
At the command line:
$ pip install mozilla-django-oidc
Quick start¶
After installation, you’ll need to do some things to get your site using
mozilla-django-oidc
.
Requirements¶
This library supports Python 3.6+ on OSX and Linux.
Acquire a client id and client secret¶
Before you can configure your application, you need to set up a client with an OpenID Connect provider (OP).
You’ll need to set up a different client for every environment you have for your site. For example, if your site has a -dev, -stage, and -prod environments, each of those has a different hostname and thus you need to set up a separate client for each one.
You need to provide your OpenID Connect provider (OP) the callback url for your
site. The URL path for the callback url is /oidc/callback/
.
Here are examples of callback urls:
http://127.0.0.1:8000/oidc/callback/
– for local developmenthttps://myapp-dev.example.com/oidc/callback/
– -dev environment for myapphttps://myapp.herokuapps.com/oidc/callback/
– my app running on Heroku
The OpenID Connect provider (OP) will then give you the following:
a client id (
OIDC_RP_CLIENT_ID
)a client secret (
OIDC_RP_CLIENT_SECRET
)
You’ll need these values for settings.
Choose the appropriate algorithm¶
Depending on your OpenID Connect provider (OP) you might need to change the
default signing algorithm from HS256
to RS256
by settings the
OIDC_RP_SIGN_ALGO
value accordingly.
For RS256
algorithm to work, you need to set either the OP signing key or
the OP JWKS Endpoint.
The corresponding settings values are:
OIDC_RP_IDP_SIGN_KEY = "<OP signing key in PEM or DER format>"
OIDC_OP_JWKS_ENDPOINT = "<URL of the OIDC OP jwks endpoint>"
If both specified, the key takes precedence.
Add settings to settings.py¶
Start by making the following changes to your settings.py
file.
# Add 'mozilla_django_oidc' to INSTALLED_APPS
INSTALLED_APPS = (
# ...
'django.contrib.auth',
'mozilla_django_oidc', # Load after auth
# ...
)
# Add 'mozilla_django_oidc' authentication backend
AUTHENTICATION_BACKENDS = (
'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
# ...
)
You also need to configure some OpenID Connect related settings too.
These values come from your OpenID Connect provider (OP).
OIDC_RP_CLIENT_ID = os.environ['OIDC_RP_CLIENT_ID']
OIDC_RP_CLIENT_SECRET = os.environ['OIDC_RP_CLIENT_SECRET']
Warning
The OpenID Connect provider (OP) provided client id and secret are secret values.
DON’T check them into version control–pull them in from the environment.
If you ever accidentally check them into version control, contact your OpenID Connect provider (OP) as soon as you can, disable that set of client id and secret, and generate a new set.
These values are specific to your OpenID Connect provider (OP)–consult their documentation for the appropriate values.
OIDC_OP_AUTHORIZATION_ENDPOINT = "<URL of the OIDC OP authorization endpoint>"
OIDC_OP_TOKEN_ENDPOINT = "<URL of the OIDC OP token endpoint>"
OIDC_OP_USER_ENDPOINT = "<URL of the OIDC OP userinfo endpoint>"
Warning
Don’t use Django’s cookie-based sessions because they might open you up to replay attacks.
You can find more info about cookie-based sessions in Django’s documentation.
These values relate to your site.
LOGIN_REDIRECT_URL = "<URL path to redirect to after login>"
LOGOUT_REDIRECT_URL = "<URL path to redirect to after logout>"
Add routing to urls.py¶
Next, edit your urls.py
and add the following:
from django.urls import path
urlpatterns = [
# ...
path('oidc/', include('mozilla_django_oidc.urls')),
# ...
]
Enable login and logout functionality in templates¶
Then you need to add the login link and the logout form to your templates.
The views are oidc_authentication_init
, oidc_logout
.
Django templates example:
{% if user.is_authenticated %}
<p>Current user: {{ user.email }}</p>
<form action="{% url 'oidc_logout' %}" method="post">
{% csrf_token %}
<input type="submit" value="logout">
</form>
{% else %}
<a href="{% url 'oidc_authentication_init' %}">Login</a>
{% endif %}
Jinja2 templates example:
{% if request.user.is_authenticated %}
<p>Current user: {{ request.user.email }}</p>
<form action="{{ url('oidc_logout') }}" method="post">
{{ csrf_input }}
<input type="submit" value="logout">
</form>
{% else %}
<a href="{{ url('oidc_authentication_init') }}">Login</a>
{% endif %}
Additional optional configuration¶
Validate ID tokens by renewing them¶
Users log into your site by authenticating with an OIDC provider. While the user is doing things on your site, it’s possible that the account that the user used to authenticate with the OIDC provider was disabled. A classic example of this is when a user quits his/her job and their LDAP account is disabled.
However, even if that account was disabled, the user’s account and session on your site will continue. In this way, a user can quit his/her job, lose access to his/her corporate account, but continue to use your website.
To handle this scenario, your website needs to know if the user’s id token with
the OIDC provider is still valid. You need to use the
mozilla_django_oidc.middleware.SessionRefresh
middleware.
To add it to your site, put it in the settings:
MIDDLEWARE = [
# middleware involving session and authentication must come first
# ...
'mozilla_django_oidc.middleware.SessionRefresh',
# ...
]
The mozilla_django_oidc.middleware.SessionRefresh
middleware will
check to see if the user’s id token has expired and if so, redirect to the OIDC
provider’s authentication endpoint for a silent re-auth. That will redirect back
to the page the user was going to.
The length of time it takes for an id token to expire is set in
settings.OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS
which defaults to 15 minutes.
Connecting OIDC user identities to Django users¶
By default, mozilla-django-oidc looks up a Django user matching the email field to the email address returned in the user info data from the OIDC provider.
This means that no two users in the Django user table can have the same email address. Since the email field is not unique, it’s possible that this can happen. Especially if you allow users to change their email address. If it ever happens, then the users in question won’t be able to authenticate.
If you want different behavior, subclass the
mozilla_django_oidc.auth.OIDCAuthenticationBackend
class and
override the filter_users_by_claims method.
For example, let’s say we store the email address in a Profile
table
in a field that’s marked unique so multiple users can’t have the same
email address. Then we could do this:
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
class MyOIDCAB(OIDCAuthenticationBackend):
def filter_users_by_claims(self, claims):
email = claims.get('email')
if not email:
return self.UserModel.objects.none()
try:
profile = Profile.objects.get(email=email)
return [profile.user]
except Profile.DoesNotExist:
return self.UserModel.objects.none()
Then you’d use the Python dotted path to that class in the
settings.AUTHENTICATION_BACKENDS
instead of
mozilla_django_oidc.auth.OIDCAuthenticationBackend
.
Creating Django users¶
Generating usernames¶
If a user logs into your site and doesn’t already have an account, by default,
mozilla-django-oidc will create a new Django user account. It will create the
User
instance filling in the username (hash of the email address) and email
fields.
If you want something different, set settings.OIDC_USERNAME_ALGO
to a Python
dotted path to the function you want to use.
The function takes in an email address as a text (Python 2 unicode or Python 3 string) and returns a text (Python 2 unicode or Python 3 string).
Here’s an example function for Python 3 that doesn’t convert the email address at all:
import unicodedata
def generate_username(email):
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
return unicodedata.normalize('NFKC', email)[:150]
See also
Changing how Django users are created¶
If your website needs to do other bookkeeping things when a new User
record
is created, then you should subclass the
mozilla_django_oidc.auth.OIDCAuthenticationBackend
class and
override the create_user method, and optionally, the update_user method.
For example, let’s say you want to populate the User
instance with other
data from the claims:
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from myapp.models import Profile
class MyOIDCAB(OIDCAuthenticationBackend):
def create_user(self, claims):
user = super(MyOIDCAB, self).create_user(claims)
user.first_name = claims.get('given_name', '')
user.last_name = claims.get('family_name', '')
user.save()
return user
def update_user(self, user, claims):
user.first_name = claims.get('given_name', '')
user.last_name = claims.get('family_name', '')
user.save()
return user
Then you’d use the Python dotted path to that class in the
settings.AUTHENTICATION_BACKENDS
instead of
mozilla_django_oidc.auth.OIDCAuthenticationBackend
.
Preventing mozilla-django-oidc from creating new Django users¶
If you don’t want mozilla-django-oidc to create Django users, you can add this setting:
OIDC_CREATE_USER = False
You might want to do this if you want to control user creation because your system requires additional process to allow people to use it.
Advanced user verification based on their claims¶
In case you need to check additional values in the user’s claims to decide
if the authentication should happen at all (included creating new users
if OIDC_CREATE_USER
is True
), then you should subclass the
mozilla_django_oidc.auth.OIDCAuthenticationBackend
class and
override the verify_claims method. It should return either True
or
False
to either continue or stop the whole authentication process.
class MyOIDCAB(OIDCAuthenticationBackend):
def verify_claims(self, claims):
verified = super(MyOIDCAB, self).verify_claims(claims)
is_admin = 'admin' in claims.get('group', [])
return verified and is_admin
Log user out of the OpenID Connect provider¶
When a user logs out, by default, mozilla-django-oidc will end the current Django session. However, the user may still have an active session with the OpenID Connect provider, in which case, the user would likely not be prompted to log back in.
Some OpenID Connect providers support a custom (not part of OIDC spec) mechanism
to end the provider’s session. We can build a function for
OIDC_OP_LOGOUT_URL_METHOD
that will redirect the user to the provider after
mozilla-django-oidc ends the Django session.
def provider_logout(request):
# See your provider's documentation for details on if and how this is
# supported
redirect_url = 'https://myprovider.com/logout'
return redirect_url
The request.build_absolute_uri
can be used if the provider requires
a return-to location.
Troubleshooting¶
mozilla-django-oidc logs using the mozilla_django_oidc
logger. Enable that
logger in settings to see logging messages to help you debug:
LOGGING = {
...
'loggers': {
'mozilla_django_oidc': {
'handlers': ['console'],
'level': 'DEBUG'
},
...
}
Make sure to use the appropriate handler for your app.