Django app for City of Helsinki user infrastructure
Django-helusers is your friendly app for bolting authentication into Django projects for City of Helsinki. Authentication schemes are based on OAuth2 and OpenID Connect (OIDC).
A baseline User
model is provided that can be used with the various authentication use cases that are supported. The model supports mapping from AD groups to Django groups based on the authentication data.
Additionally, there are optional functionalities that can be used as needed.
Functionalities for server needing (API) access token verification:
- For servers using Django REST Framework
- For servers not using Django REST Framework
Functionalities for server needing to authenticate against OIDC or OAuth2 server:
- support Django session login against OIDC or OAuth2 server, including Helsinki Tunnistus service and Azure AD
- augmented login template for Django admin, adding OIDC/OAuth2 login button
Adding django-helusers your Django project
Add django-helusers
in your project's dependencies.
Some optional features of django-helusers
have additional dependencies.
These are mentioned in their relevant sections.
Adding django-helusers Django apps
Django-helusers provides two Django apps: HelusersConfig
provides the
models and templates needed for helusers to work and HelusersAdminConfig
reconfigures Django admin to work with helusers.
Before adding the apps, you will need to remove django.contrib.admin
, as
HelusersAdminConfig
is implementation of same functionality. You will get
django.core.exceptions.ImproperlyConfigured: Application labels aren't unique, duplicates: admin
-error, if you forget this step.
Then proceed by adding these apps to your INSTALLED_APPS
in settings.py:
INSTALLED_APPS = (
"helusers.apps.HelusersConfig",
"helusers.apps.HelusersAdminConfig",
...
)
Us usual with INSTALLED_APPS
, ordering matters. HelusersConfig
must come
before HelusersAdminConfig
and anything else providing admin templates.
Unless, of course, you wish to override the admin templates provided here.
One possible gotcha is, if you've added custom views to admin without
forwarding context from each_context
to the your template. Helusers
templates expect variables from each_context
and will break if they are
missing.
Adding user model
helusers provides a baseline user model adding fields for Helsinki
specific information. As per Django best practice
you should subclass this model to make future customization easier:
from helusers.models import AbstractUser
class User(AbstractUser):
pass
and reference it in settings.py:
AUTH_USER_MODEL = "users.User"
Optional features
Django REST Framework API authentication using JWT
If you have a REST API implemented using Django REST Framework and you want to authorize access to your API using JWTs, then this might be useful to you.
API token authentication is a stateless authentication method, where every request is
authenticated by checking the signature of the included JWT token. It still
creates a persistent Django user, which is updated with the information
from the token with every request.
- Configure REST framework to use the
ApiTokenAuthentication
class in settings.py
:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"helusers.oidc.ApiTokenAuthentication",
),
}
API authentication using JWT in any setup
If you want to authorize access to your API using JWTs, but you are not using Django REST Framework, then this might be useful to you.
API token authentication is a stateless authentication method, where every request is authenticated by checking the signature of the included JWT token.
It still creates a persistent Django user, which is updated with the information from the token with every request.
Django-helusers contains a helusers.oidc.RequestJWTAuthentication
class.
It has a method called authenticate
that takes a Django HttpRequest as an argument, looks for a JWT from that request and performs authentication.
User of this class can use it in any way they need to perform authentication and/or authorization.
Check the class documentation for more details.
Token authentication settings
Some settings are needed (and some are optional) that affect how the ApiTokenAuthentication
and RequestJWTAuthentication
classes work.
OIDC_API_TOKEN_AUTH = {
"AUDIENCE": "https://api.hel.fi/auth/projects",
"ISSUER": "https://api.hel.fi/sso/openid",
"REQUIRE_API_SCOPE_FOR_AUTHENTICATION": True,
"API_AUTHORIZATION_FIELD": "scope_field",
"API_SCOPE_PREFIX": "projects",
"OIDC_CONFIG_EXPIRATION_TIME": 600,
"ALLOWED_ALGORITHMS": ["RS256"],
}
OIDC back channel logout endpoint
Django-helusers provides an OIDC back channel logout endpoint implementation.
By default the OIDC back channel logout endpoint is disabled. You can enable it in your project's settings:
HELUSERS_BACK_CHANNEL_LOGOUT_ENABLED = True
OIDC_API_TOKEN_AUTH = {
"ISSUER": "https://api.hel.fi/sso/openid",
"AUDIENCE": "https://api.hel.fi/auth/projects",
}
You will also need to add Django-helusers URLs to your URL dispatcher configuration:
urlpatterns = [
...
path("helauth/", include("helusers.urls")),
...
]
With these settings your project now provides an endpoint at https://<your-domain>/helauth/logout/oidc/backchannel/
that responds to the OIDC back channel logout requests.
When the endpoint receives a valid request, it stores information about the logout event to the database. This information is used when authentication for other requests is performed. The helusers.oidc.RequestJWTAuthentication
class that performs authentication based on a JWT bearer token, checks if the token's session has been terminated (by a logout event), and if that's the case, it doesn't authenticate the caller.
Logout event callback
The project using the OIDC back channel logout functionality has an option to attach a callback into the logout event handler. This is done by telling Django-helusers where this callback is located. Configure it in your project's settings:
HELUSERS_BACK_CHANNEL_LOGOUT_CALLBACK = "myproject.utils.logout_callback"
When a valid logout event is received, the callback is called. The callback receives two keyword arguments:
request
: the HttpRequest object describing the request to the logout endpointjwt
: a helusers.jwt.JWT
instance of the logout token
The callback can affect the result of the back channel logout event handling by returning an HttpResponse instance with a status code between 400 and 599 inclusive. If such a response object is returned by the callback, the logout event handling is terminated and the response is sent to the requester. Any other kind of return value from the callback is ignored.
Adding Tunnistamo authentication
django-helusers ships with backend for authenticating against Tunnistamo
using OIDC. Configuring this includes a Tunnistamo login button to the admin login screen.
There is also a deprecated legacy OAuth2 backend using
allauth framework.
Include social-auth-app-django
in your project's dependencies.
Add social_django
into your INSTALLED_APPS
setting:
INSTALLED_APPS = (
...
"social_django",
...
)
Typically you would want to support authenticating using both OIDC and local
database tables. Local users are useful for initial django admin login, before
you've delegated permissions to users coming through OIDC.
Add backend configuration to your settings.py
:
AUTHENTICATION_BACKENDS = [
"helusers.tunnistamo_oidc.TunnistamoOIDCAuth",
"django.contrib.auth.backends.ModelBackend",
]
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL
is the default landing URL after succesful login, if your
form did not specify anything else.
LOGOUT_REDIRECT_URL
is the same for logout. django-helusers requires this
to be set.
Configure social_django
authentication pipeline
to handle the users. Django-helusers provides a default pipeline that could well
suit your needs. Use it by importing it into your settings.py
:
from helusers.defaults import SOCIAL_AUTH_PIPELINE
If the default pipeline isn't suitable for your needs as is, build your pipeline
by hand and set the SOCIAL_AUTH_PIPELINE
setting to it. You can use the default
pipeline as an inspiration and use the functions from helusers.pipeline
in your
own pipeline.
You will also need to add URLs for social_django
& helusers
to your URL
dispatcher configuration (urls.py
):
urlpatterns = [
...
path("pysocial/", include("social_django.urls", namespace="social")),
path("helauth/", include("helusers.urls")),
...
]
You can change the paths if they conflict with your application.
Finally, because of the pipeline set earlier you will also need to configure your
SESSION_SERIALIZER. helusers stores the access token expiration time as a datetime
which is not serializable to JSON, so Django needs to be configured to use the
provided TunnistamoOIDCSerializer.
SESSION_SERIALIZER = "helusers.sessions.TunnistamoOIDCSerializer"
Django session login
Django session login is the usual login to Django that sets up a session
and is typically implemented using a browser cookie. This is usually done
using form with username & password fields. Django-helusers adds another
path that delegates the login to an OIDC provider. User logs in at the
provider and, upon successful return, a Django session is created for them.
For us, the main use case has been allowing logins to Django admin.
To support session login Django-helusers needs three settings that must
be configured both at Helsinki OIDC provider and your project instance.
The settings are:
- client ID
- client secret
- Tunnistamo OIDC endpoint
Client
is OAuth2 / OIDC name for anything wanting to authenticate
users. Thus your application would be a client
Additionally you will need to provide your "callback URL" to the folks
configuring Tunnistamo. This is implemented by python-social-auth
and
will, by default, be https://app.domain/auth/complete/tunnistamo/
. During
development on your own laptop your app.domain
would be localhost
.
After you've received your client ID, client secret and Tunnistamo OIDC
endpoint you would configure them as follows:
SOCIAL_AUTH_TUNNISTAMO_KEY = "https://i/am/clientid/in/url/style"
SOCIAL_AUTH_TUNNISTAMO_SECRET = "iamyoursecret"
SOCIAL_AUTH_TUNNISTAMO_OIDC_ENDPOINT = "https://tunnistamo.example.com/"
Note that client ID
becomes KEY
and client secret
becomes SECRET
.
Active Directory groups
Helusers can sync users AD groups to local Django groups when using an AD
login method in Tunnistamo. To enable groups sync you should add "ad_groups"
scope to the Tunnistamo OIDC authorize call. It can be done by adding
the following to the settings:
SOCIAL_AUTH_TUNNISTAMO_SCOPE = "ad_groups"
That setting will add "ad_groups" scope to the default social auth scopes
"openid profile email". If you would like to modify the default social
auth scopes you can set all of the scopes in the SOCIAL_AUTH_TUNNISTAMO_SCOPE
setting and set SOCIAL_AUTH_TUNNISTAMO_IGNORE_DEFAULT_SCOPE
to True
.
Additionally, the client in Tunnistamo should be configured with AD groups
enabled.
When the users returns from Tunnistamo with "ad_groups" claim set Helusers will
add all of the groups as an instance of ADGroup
model to the database.
Then, Helusers will add any missing ADGroups to the users' ad_groups-relation
and remove any ADGroups the user is not a member of anymore.
To use groups in Django permissions, you should use the Django admin view
(HELSINKI USERS > AD Group Mappings) to set mappings between ADGroups and
Groups. Helusers will then add the user to Django groups that are mapped
to their AD Groups.
Note that after creating mappings you cannot manually add a user to a mapped
group if they are not a member of the corresponding AD group because the
group will be removed the next time the user logs in.
Adding tunnistamo URL to template context
If you need to access the Tunnistamo API from your JS code, you can include
the Tunnistamo base URL in your template context using helusers's context processor:
TEMPLATES = [
{
"OPTIONS": {
"context_processors": [
"helusers.context_processors.settings"
]
}
}
]
Carrying language preference from your application to Tunnistamo
Tunnistamo (per the OIDC specs) allows clients to specify the language used for
the login process. This allows you to carry your applications language setting
to the login screens presented by Tunnistamo.
Configure python-social-auth
to pass the necessary argument through its
login view:
SOCIAL_AUTH_TUNNISTAMO_AUTH_EXTRA_ARGUMENTS = {"ui_locales": "fi"}
fi
there is the language code that will be used when no language is requested, so change it if you you prefer some
other default language. If you don't want to set a default language at all, use an empty string ""
as the language
code.
When this setting is in place, languages can be requested using query param ui_locales=<language code>
when starting
the login process, for example in your template
<a href="{% url 'helusers:auth_login' %}?next=/foobar/&ui_locales=en">Login in English</a>
Disabling password logins
If you're not allowing users to log in with passwords, you may disable the
username/password form from Django admin login page by setting HELUSERS_PASSWORD_LOGIN_DISABLED
to True
.
Migrating old user from Tunnistamo to Keycloak
By default, the migration logic is configured to support migrating users from Tunnistamo
AD authentication to Keycloak AD authentication. The migration should be tested by the
service before enabling it in production. This migration logic most likely shouldn't be
configured for other authentication methods besides AD (i.e. staff/admin) users.
When transitioning from one authentication provider to another, it is possible to
migrate the old user data for the new user with a different UUID. Migration is done by
finding the old user instance and replacing its UUID with the new one from the token
payload. So instead of creating a new user instance, we update the old one. Migration
happens one user at a time upon login.
Feature can be configured using the following settings.
HELUSERS_USER_MIGRATE_ENABLED
: Enable the feature. Defaults to False
.HELUSERS_USER_MIGRATE_EMAIL_DOMAINS
: Whitelisted email domains for migration.
Defaults to ["hel.fi"]
.HELUSERS_USER_MIGRATE_AMRS
which authentication methods are used for migration.
Defaults to ["helsinkiad"]
.
Migration logic is only run on certain conditions:
- Correct authentication method is used (AMR-claim)
- Email domain is correct
- User with the new UUID doesn't exist yet
- Old user is found by email
- Username has been generated by helusers.utils.uuid_to_username for the old user
Development
Virtual Python environment can be used. For example:
python3 -m venv .venv
source .venv/bin/activate
Install package requirements:
pip install -e .
Install development requirements:
pip install -r requirements-test.txt
Running tests
pytest
You can run the tests against multiple environments by using tox.
Install tox
globally and run:
tox
Code format
This project uses
black
,
flake8
and
isort
for code formatting and quality checking. Project follows the basic
black config, without any modifications.
Basic black
commands:
- To let
black
do its magic: black .
- To see which files
black
would change: black --check .
pre-commit
can be used to install and
run all the formatting tools as git hooks automatically before a
commit.
Git blame ignore refs
Project includes a .git-blame-ignore-revs
file for ignoring certain commits from git blame
.
This can be useful for ignoring e.g. formatting commits, so that it is more clear from git blame
where the actual code change came from. Configure your git to use it for this project with the
following command:
git config blame.ignoreRevsFile .git-blame-ignore-revs
Commit message format
New commit messages must adhere to the Conventional Commits
specification, and line length is limited to 72 characters.
When pre-commit
is in use, commitlint
checks new commit messages for the correct format.