From b19657ff08a520a28913db7b805c0d9b90f55386 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Tue, 17 Oct 2023 22:14:47 +0200 Subject: [PATCH] authentication: add oidc support aiming for authentik integration --- jdav_web/jdav_web/oidc.py | 40 +++++++++++++++ jdav_web/jdav_web/settings/__init__.py | 1 + .../settings/components/authentication.py | 51 +++++++++++++++++++ jdav_web/jdav_web/settings/components/base.py | 25 +-------- jdav_web/jdav_web/urls.py | 9 ++++ requirements.txt | 7 +++ 6 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 jdav_web/jdav_web/oidc.py create mode 100644 jdav_web/jdav_web/settings/components/authentication.py diff --git a/jdav_web/jdav_web/oidc.py b/jdav_web/jdav_web/oidc.py new file mode 100644 index 0000000..7df60b2 --- /dev/null +++ b/jdav_web/jdav_web/oidc.py @@ -0,0 +1,40 @@ +from django.conf import settings +from mozilla_django_oidc.auth import OIDCAuthenticationBackend + + +class MyOIDCAB(OIDCAuthenticationBackend): + def filter_users_by_claims(self, claims): + username = claims.get(settings.OIDC_CLAIM_USERNAME) + if not username: + return self.UserModel.objects.none() + + return self.UserModel.objects.filter(username=username) + + def get_username(self, claims): + username = claims.get(settings.OIDC_CLAIM_USERNAME, '') + + if not username: + return super(MyOIDCAB, self).get_username(claims) + + return username + + def get_userinfo(self, access_token, id_token, payload): + return super(MyOIDCAB, self).get_userinfo(access_token, id_token, payload) + + def create_user(self, claims): + user = super(MyOIDCAB, self).create_user(claims) + return self.update_user(user, claims) + + def update_user(self, user, claims): + user.first_name = claims.get(settings.OIDC_CLAIM_FIRST_NAME, '') + user.last_name = claims.get(settings.OIDC_CLAIM_LAST_NAME, '') + groups = claims.get('groups', []) + + if settings.OIDC_GROUP_STAFF in groups: + user.is_staff = True + if settings.OIDC_GROUP_SUPERUSER in groups: + user.is_superuser = True + + user.save() + + return user diff --git a/jdav_web/jdav_web/settings/__init__.py b/jdav_web/jdav_web/settings/__init__.py index 85bc5a1..b85caa4 100644 --- a/jdav_web/jdav_web/settings/__init__.py +++ b/jdav_web/jdav_web/settings/__init__.py @@ -16,6 +16,7 @@ import os base_settings = [ 'local.py', 'components/base.py', + 'components/authentication.py', 'components/database.py', 'components/cache.py', 'components/jet.py', diff --git a/jdav_web/jdav_web/settings/components/authentication.py b/jdav_web/jdav_web/settings/components/authentication.py new file mode 100644 index 0000000..8da5651 --- /dev/null +++ b/jdav_web/jdav_web/settings/components/authentication.py @@ -0,0 +1,51 @@ +# Authentication + +AUTHENTICATION_BACKENDS = ( + 'jdav_web.oidc.MyOIDCAB', + 'django.contrib.auth.backends.ModelBackend', + 'rules.permissions.ObjectPermissionBackend', +) + +# Use Open ID Connect if possible +OIDC_ENABLED = '1' == os.environ.get('OIDC_ENABLED', '0') + +# OIDC configuration +OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID', '') +OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET', '') +OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT', '') +OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_ENDPOINT', '') +OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT', '') +OIDC_OP_JWKS_ENDPOINT = os.environ.get('OIDC_OP_JWKS_ENDPOINT', '') + +OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'RS256') +OIDC_RP_SCOPES = os.environ.get('ODIC_RP_SCOPES', 'openid email profile') + +OIDC_CLAIM_USERNAME = os.environ.get('OIDC_CLAIM_USERNAME', 'username') +OIDC_CLAIM_FIRST_NAME = os.environ.get('OIDC_CLAIM_FIRST_NAME', 'given_name') +OIDC_CLAIM_LAST_NAME = os.environ.get('OIDC_CLAIM_LAST_NAME', 'last_name') +OIDC_GROUP_STAFF = os.environ.get('OIDC_GROUP_STAFF', 'staff') +OIDC_GROUP_SUPERUSER = os.environ.get('OIDC_GROUP_STAFF', 'superuser') + +LOGIN_REDIRECT_URL = "/kompass" +LOGOUT_REDIRECT_URL = "/" + +# default login URL, is not used if OIDC is not enabled +LOGIN_URL = "/oidc/authenticate/" + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] diff --git a/jdav_web/jdav_web/settings/components/base.py b/jdav_web/jdav_web/settings/components/base.py index bbb8945..d7bd239 100644 --- a/jdav_web/jdav_web/settings/components/base.py +++ b/jdav_web/jdav_web/settings/components/base.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ 'jet', 'django.contrib.admin', 'django.contrib.auth', + 'mozilla_django_oidc', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', @@ -73,6 +74,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.cache.FetchFromCacheMiddleware', + 'mozilla_django_oidc.middleware.SessionRefresh', ] ROOT_URLCONF = 'jdav_web.urls' @@ -95,29 +97,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'jdav_web.wsgi.application' -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'rules.permissions.ObjectPermissionBackend', -) - -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ diff --git a/jdav_web/jdav_web/urls.py b/jdav_web/jdav_web/urls.py index d38e369..e022680 100644 --- a/jdav_web/jdav_web/urls.py +++ b/jdav_web/jdav_web/urls.py @@ -15,6 +15,7 @@ Including another URLconf """ from django.urls import re_path, include from django.contrib import admin +from django.contrib.admin.views.decorators import staff_member_required from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.conf import settings @@ -26,6 +27,14 @@ urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) admin.site.index_title = _('Startpage') admin.site.site_header = 'Kompass' +if settings.OIDC_ENABLED: + admin.site.login = staff_member_required( + admin.site.login, login_url=settings.LOGIN_URL + ) + urlpatterns += i18n_patterns( + re_path(r'^oidc/', include('mozilla_django_oidc.urls')), + ) + urlpatterns += i18n_patterns( re_path(r'^kompass/?', admin.site.urls), re_path(r'^jet/', include('jet.urls', 'jet')), # Django JET URLS diff --git a/requirements.txt b/requirements.txt index 33da12c..fdfc1d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,15 @@ billiard==3.6.4.0 bleach==6.0.0 celery==5.2.3 certifi==2021.10.8 +cffi==1.16.0 charset-normalizer==2.0.10 click==8.0.3 click-didyoumean==0.3.0 click-plugins==1.1.1 click-repl==0.2.0 +coverage==7.2.3 cron-descriptor==1.2.35 +cryptography==41.0.4 Deprecated==1.2.13 Django==4.0.1 django-appconf==1.0.5 @@ -24,13 +27,17 @@ django-split-settings==1.2.0 django-timezone-field==5.0 idna==3.3 importlib-metadata==6.2.0 +josepy==1.13.0 kombu==5.2.3 Markdown==3.4.3 +mozilla-django-oidc==3.0.0 mysqlclient==2.1.0 packaging==21.3 Pillow==9.0.0 prompt-toolkit==3.0.24 +pycparser==2.21 pymemcache==4.0.0 +pyOpenSSL==23.2.0 pyparsing==3.0.6 python-crontab==2.7.1 python-dateutil==2.8.2