From 38c4d337910ccc37e2ea022d16562b081b44a1ef Mon Sep 17 00:00:00 2001 From: Lebaud Antoine Date: Thu, 15 Feb 2024 11:00:30 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20support=20Authorization=20?= =?UTF-8?q?code=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate 'mozilla-django-oidc' dependency, to support Authorization Code flow, which is required by Agent Connect. Thus, we provide a secure back channel OIDC flow, and return to the client only a session cookie. Done: - Replace JWT authentication by Session based authentication in DRF - Update Django settings to make OIDC configurations easily editable - Add 'mozilla-django-oidc' routes to our router - Implement a custom Django Authentication class to adapt 'mozilla-django-oidc' to our needs 'mozilla-django-oidc' routes added are: - /authenticate - /callback (the redirect_uri called back by the Idp) - /logout --- .github/workflows/people.yml | 2 +- env.d/development/common.dist | 22 ++- src/backend/core/api/utils.py | 14 -- src/backend/core/authentication.py | 126 +++++++++----- src/backend/core/models.py | 39 ----- .../test_authentication_get_or_create_user.py | 164 ++++++++++++++++++ .../tests/test_models_oidc_user_getter.py | 107 ------------ src/backend/core/tokens.py | 10 -- src/backend/people/api_urls.py | 2 + src/backend/people/settings.py | 97 +++++++---- src/backend/pyproject.toml | 2 +- 11 files changed, 335 insertions(+), 250 deletions(-) delete mode 100644 src/backend/core/api/utils.py create mode 100644 src/backend/core/tests/test_authentication_get_or_create_user.py delete mode 100644 src/backend/core/tests/test_models_oidc_user_getter.py delete mode 100644 src/backend/core/tokens.py diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 5734a3d..9b07a6c 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -216,7 +216,7 @@ jobs: DJANGO_CONFIGURATION: Test DJANGO_SETTINGS_MODULE: people.settings DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly - DJANGO_JWT_PRIVATE_SIGNING_KEY: ThisIsAnExampleKeyForDevPurposeOnly + OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only DB_HOST: localhost DB_NAME: people DB_USER: dinum diff --git a/env.d/development/common.dist b/env.d/development/common.dist index ac1f9d8..91d0b1a 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -7,9 +7,6 @@ DJANGO_SUPERUSER_PASSWORD=admin # Python PYTHONPATH=/app -#JWT -DJANGO_JWT_PRIVATE_SIGNING_KEY=ThisIsAnExampleKeyForDevPurposeOnly - # People settings # Mail @@ -19,5 +16,20 @@ DJANGO_EMAIL_PORT=1025 # Backend url PEOPLE_BASE_URL="http://localhost:8072" -# Keycloak -SIMPLE_JWT_JWK_URL="http://keycloak:8080/realms/people/protocol/openid-connect/certs" +# OIDC +OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/people/protocol/openid-connect/certs +OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8083/realms/people/protocol/openid-connect/auth +OIDC_OP_TOKEN_ENDPOINT=http://nginx:8083/realms/people/protocol/openid-connect/token +OIDC_OP_USER_ENDPOINT=http://nginx:8083/realms/people/protocol/openid-connect/userinfo + +OIDC_RP_CLIENT_ID=people +OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly +OIDC_RP_SIGN_ALGO=RS256 +OIDC_RP_SCOPES="openid email" + +LOGIN_REDIRECT_URL=http://localhost:3000 +LOGIN_REDIRECT_URL_FAILURE=http://localhost:3000 +LOGOUT_REDIRECT_URL=http://localhost:3000 + +OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"] +OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"} diff --git a/src/backend/core/api/utils.py b/src/backend/core/api/utils.py deleted file mode 100644 index 4c81cd7..0000000 --- a/src/backend/core/api/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Utils that can be useful throughout the People core app -""" -from rest_framework_simplejwt.tokens import RefreshToken - - -def get_tokens_for_user(user): - """Get JWT tokens for user authentication.""" - refresh = RefreshToken.for_user(user) - - return { - "refresh": str(refresh), - "access": str(refresh.access_token), - } diff --git a/src/backend/core/authentication.py b/src/backend/core/authentication.py index 79b068b..2c16142 100644 --- a/src/backend/core/authentication.py +++ b/src/backend/core/authentication.py @@ -1,59 +1,107 @@ """Authentication for the People core app.""" -from django.conf import settings -from django.utils.functional import SimpleLazyObject -from django.utils.module_loading import import_string + +from django.core.exceptions import SuspiciousOperation +from django.db import models from django.utils.translation import gettext_lazy as _ -from drf_spectacular.authentication import SessionScheme, TokenScheme -from drf_spectacular.plumbing import build_bearer_security_scheme_object -from rest_framework import authentication -from rest_framework_simplejwt.authentication import JWTAuthentication +import requests +from mozilla_django_oidc.auth import ( + OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend, +) + +from .models import Identity -class DelegatedJWTAuthentication(JWTAuthentication): - """Override JWTAuthentication to create missing users on the fly.""" +class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend): + """Custom OpenID Connect (OIDC) Authentication Backend. - def get_user(self, validated_token): + This class overrides the default OIDC Authentication Backend to accommodate differences + in the User and Identity models, and handles signed and/or encrypted UserInfo response. + """ + + def get_userinfo(self, access_token, id_token, payload): + """Return user details dictionary. + + Parameters: + - access_token (str): The access token. + - id_token (str): The id token (unused). + - payload (dict): The token payload (unused). + + Note: The id_token and payload parameters are unused in this implementation, + but were kept to preserve base method signature. + + Note: It handles signed and/or encrypted UserInfo Response. It is required by + Agent Connect, which follows the OIDC standard. It forces us to override the + base method, which deal with 'application/json' response. + + Returns: + - dict: User details dictionary obtained from the OpenID Connect user endpoint. """ - Return the user related to the given validated token, creating or updating it if necessary. + + user_response = requests.get( + self.OIDC_OP_USER_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, + verify=self.get_settings("OIDC_VERIFY_SSL", True), + timeout=self.get_settings("OIDC_TIMEOUT", None), + proxies=self.get_settings("OIDC_PROXY", None), + ) + user_response.raise_for_status() + userinfo = self.verify_token(user_response.text) + return userinfo + + def get_or_create_user(self, access_token, id_token, payload): + """Return a User based on userinfo. Get or create a new user if no user matches the Sub. + + Parameters: + - access_token (str): The access token. + - id_token (str): The ID token. + - payload (dict): The user payload. + + Returns: + - User: An existing or newly created User instance. + + Raises: + - Exception: Raised when user creation is not allowed and no existing user is found. """ - get_user = import_string(settings.JWT_USER_GETTER) - return SimpleLazyObject(lambda: get_user(validated_token)) + user_info = self.get_userinfo(access_token, id_token, payload) -class OpenApiJWTAuthenticationExtension(TokenScheme): - """Extension for specifying JWT authentication schemes.""" + email = user_info.get("email") + sub = user_info.get("sub") - target_class = "core.authentication.DelegatedJWTAuthentication" - name = "DelegatedJWTAuthentication" + if sub is None: + raise SuspiciousOperation( + _("User info contained no recognizable user identification") + ) - def get_security_definition(self, auto_schema): - """Return the security definition for JWT authentication.""" - return build_bearer_security_scheme_object( - header_name="Authorization", - token_prefix="Bearer", # noqa S106 + user = ( + self.UserModel.objects.filter(identities__sub=sub) + .annotate(identity_email=models.F("identities__email")) + .distinct() + .first() ) + if user: + if email and email != user.identity_email: + Identity.objects.filter(sub=sub).update(email=email) -class SessionAuthenticationWithAuthenticateHeader(authentication.SessionAuthentication): - """ - This class is needed, because REST Framework's default SessionAuthentication does - never return 401's, because they cannot fill the WWW-Authenticate header with a - valid value in the 401 response. As a result, we cannot distinguish calls that are - not unauthorized (401 unauthorized) and calls for which the user does not have - permission (403 forbidden). - See https://github.com/encode/django-rest-framework/issues/5968 + elif self.get_settings("OIDC_CREATE_USER", True): + user = self.create_user(user_info) - We do set authenticate_header function in SessionAuthentication, so that a value - for the WWW-Authenticate header can be retrieved and the response code is - automatically set to 401 in case of unauthenticated requests. - """ + return user - def authenticate_header(self, request): - return "Session" + def create_user(self, claims): + """Return a newly created User instance.""" + email = claims.get("email") + sub = claims.get("sub") -class OpenApiSessionAuthenticationExtension(SessionScheme): - """Extension for specifying session authentication schemes.""" + if sub is None: + raise SuspiciousOperation( + _("Claims contained no recognizable user identification") + ) - target_class = "core.api.authentication.SessionAuthenticationWithAuthenticateHeader" + user = self.UserModel.objects.create(password="!", email=email) # noqa: S106 + Identity.objects.create(user=user, sub=sub, email=email) + + return user diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 4a5e6e8..dc19738 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -17,8 +17,6 @@ from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ import jsonschema -from rest_framework_simplejwt.exceptions import InvalidToken -from rest_framework_simplejwt.settings import api_settings from timezone_field import TimeZoneField current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -471,40 +469,3 @@ class Invitation(BaseModel): """Calculate if invitation is still valid or has expired.""" validity_duration = timedelta(seconds=settings.INVITATION_VALIDITY_DURATION) return timezone.now() > (self.created_at + validity_duration) - - -def oidc_user_getter(validated_token): - """ - Given a valid OIDC token , retrieve, create or update corresponding user/contact/email from db. - - The token is expected to have the following fields in payload: - - sub - - email - - ... - """ - try: - user_id = validated_token[api_settings.USER_ID_CLAIM] - except KeyError as exc: - raise InvalidToken( - _("Token contained no recognizable user identification") - ) from exc - - try: - email_param = {"email": validated_token["email"]} - except KeyError: - email_param = {} - - user = ( - User.objects.filter(identities__sub=user_id) - .annotate(identity_email=models.F("identities__email")) - .distinct() - .first() - ) - - if user is None: - user = User.objects.create(password="!", **email_param) # noqa: S106 - Identity.objects.create(user=user, sub=user_id, **email_param) - elif email_param and validated_token["email"] != user.identity_email: - Identity.objects.filter(sub=user_id).update(email=validated_token["email"]) - - return user diff --git a/src/backend/core/tests/test_authentication_get_or_create_user.py b/src/backend/core/tests/test_authentication_get_or_create_user.py new file mode 100644 index 0000000..7f877bb --- /dev/null +++ b/src/backend/core/tests/test_authentication_get_or_create_user.py @@ -0,0 +1,164 @@ +"""Unit tests for the `get_or_create_user` function.""" + +from django.core.exceptions import SuspiciousOperation + +import pytest + +from core import models +from core.authentication import OIDCAuthenticationBackend +from core.factories import IdentityFactory + +pytestmark = pytest.mark.django_db + + +def test_authentication_getter_existing_user_no_email( + django_assert_num_queries, monkeypatch +): + """ + If an existing user matches the user's info sub, the user should be returned. + """ + + klass = OIDCAuthenticationBackend() + + # Create a user and its identity + identity = IdentityFactory() + + # Create multiple identities for a user + for _ in range(5): + IdentityFactory(user=identity.user) + + def get_userinfo_mocked(*args): + return {"sub": identity.sub} + + monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) + + with django_assert_num_queries(1): + user = klass.get_or_create_user( + access_token="test-token", id_token=None, payload=None + ) + + identity.refresh_from_db() + assert user == identity.user + + +def test_authentication_getter_existing_user_with_email( + django_assert_num_queries, monkeypatch +): + """ + When the user's info contains an email and targets an existing user, + it should update the email on the identity but not on the user. + """ + klass = OIDCAuthenticationBackend() + + identity = IdentityFactory() + + # Create multiple identities for a user + for _ in range(5): + IdentityFactory(user=identity.user) + + user_email = identity.user.email + assert models.User.objects.count() == 1 + + def get_userinfo_mocked(*args): + return {"sub": identity.sub, "email": identity.email} + + monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) + + # Only 1 query if the email has not changed + with django_assert_num_queries(1): + user = klass.get_or_create_user( + access_token="test-token", id_token=None, payload=None + ) + + new_email = "test@fooo.com" + + def get_userinfo_mocked_new_email(*args): + return {"sub": identity.sub, "email": new_email} + + monkeypatch.setattr( + OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked_new_email + ) + + # Additional update query if the email has changed + with django_assert_num_queries(2): + user = klass.get_or_create_user( + access_token="test-token", id_token=None, payload=None + ) + + identity.refresh_from_db() + assert identity.email == new_email + + assert models.User.objects.count() == 1 + assert user == identity.user + assert user.email == user_email + + +def test_authentication_getter_new_user_no_email(monkeypatch): + """ + If no user matches the user's info sub, a user should be created. + User's info doesn't contain an email, created user's email should be empty. + """ + klass = OIDCAuthenticationBackend() + + def get_userinfo_mocked(*args): + return {"sub": "123"} + + monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) + + user = klass.get_or_create_user( + access_token="test-token", id_token=None, payload=None + ) + + identity = user.identities.get() + assert identity.sub == "123" + assert identity.email is None + + assert user.email is None + assert user.password == "!" + assert models.User.objects.count() == 1 + + +def test_authentication_getter_new_user_with_email(monkeypatch): + """ + If no user matches the user's info sub, a user should be created. + User's info contains an email, created user's email should be set. + """ + klass = OIDCAuthenticationBackend() + + email = "people@example.com" + + def get_userinfo_mocked(*args): + return {"sub": "123", "email": email} + + monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) + + user = klass.get_or_create_user( + access_token="test-token", id_token=None, payload=None + ) + + identity = user.identities.get() + assert identity.sub == "123" + assert identity.email == email + + assert user.email == email + assert models.User.objects.count() == 1 + + +def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkeypatch): + """The user's info doesn't contain a sub.""" + klass = OIDCAuthenticationBackend() + + def get_userinfo_mocked(*args): + return { + "test": "123", + } + + monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) + + with django_assert_num_queries(0), pytest.raises( + SuspiciousOperation, + match="User info contained no recognizable user identification", + ): + klass.get_or_create_user(access_token="test-token", id_token=None, payload=None) + + assert models.User.objects.exists() is False diff --git a/src/backend/core/tests/test_models_oidc_user_getter.py b/src/backend/core/tests/test_models_oidc_user_getter.py deleted file mode 100644 index 8135473..0000000 --- a/src/backend/core/tests/test_models_oidc_user_getter.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Unit tests for the `oidc_user_getter` function.""" -import pytest -from rest_framework_simplejwt.exceptions import InvalidToken -from rest_framework_simplejwt.tokens import AccessToken - -from core import factories, models - -pytestmark = pytest.mark.django_db - - -def test_models_oidc_user_getter_existing_user_no_email(django_assert_num_queries): - """ - When a valid token is passed, an existing user matching the token's sub should be returned. - """ - identity = factories.IdentityFactory() - factories.IdentityFactory(user=identity.user) # another identity for the user - token = AccessToken() - token["sub"] = str(identity.sub) - - with django_assert_num_queries(1): - user = models.oidc_user_getter(token) - - identity.refresh_from_db() - assert user == identity.user - - -def test_models_oidc_user_getter_existing_user_with_email(django_assert_num_queries): - """ - When the valid token passed contains an email and targets an existing user, - it should update the email on the identity but not on the user. - """ - identity = factories.IdentityFactory() - factories.IdentityFactory(user=identity.user) # another identity for the user - user_email = identity.user.email - assert models.User.objects.count() == 1 - - token = AccessToken() - token["sub"] = str(identity.sub) - - # Only 1 query if the email has not changed - token["email"] = identity.email - with django_assert_num_queries(1): - user = models.oidc_user_getter(token) - - # Additional update query if the email has changed - new_email = "people@example.com" - token["email"] = new_email - with django_assert_num_queries(2): - user = models.oidc_user_getter(token) - - identity.refresh_from_db() - assert identity.email == new_email - - assert models.User.objects.count() == 1 - assert user == identity.user - assert user.email == user_email - - -def test_models_oidc_user_getter_new_user_no_email(): - """ - When a valid token is passed, a user should be created if the sub - does not match any existing user. - """ - token = AccessToken() - token["sub"] = "123" - - user = models.oidc_user_getter(token) - - identity = user.identities.get() - assert identity.sub == "123" - assert identity.email is None - - assert user.email is None - assert user.password == "!" - assert models.User.objects.count() == 1 - - -def test_models_oidc_user_getter_new_user_with_email(): - """ - When the valid token passed contains an email and a new user is created, - the email should be set on the user and on the identity. - """ - email = "people@example.com" - token = AccessToken() - token["sub"] = "123" - token["email"] = email - - user = models.oidc_user_getter(token) - - identity = user.identities.get() - assert identity.sub == "123" - assert identity.email == email - - assert user.email == email - assert models.User.objects.count() == 1 - - -def test_models_oidc_user_getter_invalid_token(django_assert_num_queries): - """The token passed in argument should contain the configured user id claim.""" - token = AccessToken() - - with django_assert_num_queries(0), pytest.raises( - InvalidToken, match="Token contained no recognizable user identification" - ): - models.oidc_user_getter(token) - - assert models.User.objects.exists() is False diff --git a/src/backend/core/tokens.py b/src/backend/core/tokens.py deleted file mode 100644 index 8b054b4..0000000 --- a/src/backend/core/tokens.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Tokens for People's core app.""" -from rest_framework_simplejwt.settings import api_settings -from rest_framework_simplejwt.tokens import Token - - -class BearerToken(Token): - """Bearer token as emitted by Keycloak OIDC for example.""" - - token_type = "Bearer" # noqa: S105 - lifetime = api_settings.ACCESS_TOKEN_LIFETIME diff --git a/src/backend/people/api_urls.py b/src/backend/people/api_urls.py index d3ddb75..6077092 100644 --- a/src/backend/people/api_urls.py +++ b/src/backend/people/api_urls.py @@ -2,6 +2,7 @@ from django.conf import settings from django.urls import include, path, re_path +from mozilla_django_oidc.urls import urlpatterns as oidc_urls from rest_framework.routers import DefaultRouter from core.api import viewsets @@ -26,6 +27,7 @@ urlpatterns = [ include( [ *router.urls, + *oidc_urls, re_path( r"^teams/(?P[0-9a-z-]*)/", include(team_related_router.urls), diff --git a/src/backend/people/settings.py b/src/backend/people/settings.py index 88fa773..2bd5588 100755 --- a/src/backend/people/settings.py +++ b/src/backend/people/settings.py @@ -180,6 +180,7 @@ class Base(Configuration): AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", + "core.authentication.OIDCAuthenticationBackend", ] # Django's applications from the highest priority to the lowest @@ -203,6 +204,8 @@ class Base(Configuration): "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", + # OIDC third party + "mozilla_django_oidc", ] # Cache @@ -212,7 +215,8 @@ class Base(Configuration): REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( - "core.authentication.DelegatedJWTAuthentication", + "mozilla_django_oidc.contrib.drf.OIDCAuthentication", + "rest_framework.authentication.SessionAuthentication", ), "DEFAULT_PARSER_CLASSES": [ "rest_framework.parsers.JSONParser", @@ -243,34 +247,6 @@ class Base(Configuration): "REDOC_DIST": "SIDECAR", } - # Simple JWT - SIMPLE_JWT = { - "ALGORITHM": values.Value( - "RS256", environ_name="SIMPLE_JWT_ALGORITHM", environ_prefix=None - ), - "JWK_URL": values.Value( - None, environ_name="SIMPLE_JWT_JWK_URL", environ_prefix=None - ), - "SIGNING_KEY": values.Value( - None, environ_name="SIMPLE_JWT_SIGNING_KEY", environ_prefix=None - ), - "VERIFYING_KEY": values.Value( - None, environ_name="SIMPLE_JWT_VERIFYING_KEY", environ_prefix=None - ), - "AUTH_HEADER_TYPES": ("Bearer",), - "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", - "TOKEN_TYPE_CLAIM": "typ", - "USER_ID_FIELD": "sub", - "USER_ID_CLAIM": "sub", - "AUTH_TOKEN_CLASSES": ("core.tokens.BearerToken",), - } - - JWT_USER_GETTER = values.Value( - "core.models.oidc_user_getter", - environ_name="PEOPLE_JWT_USER_GETTER", - environ_prefix=None, - ) - # Mail EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend") EMAIL_HOST = values.Value(None) @@ -301,6 +277,63 @@ class Base(Configuration): CELERY_BROKER_URL = values.Value("redis://redis:6379/0") CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}) + # Session + SESSION_ENGINE = "django.contrib.sessions.backends.cache" + SESSION_COOKIE_AGE = 60 * 60 * 12 # 12 hours to match Agent Connect + + # OIDC - Authorization Code Flow + OIDC_CREATE_USER = values.BooleanValue( + default=True, + environ_name="OIDC_CREATE_USER", + ) + OIDC_RP_SIGN_ALGO = values.Value( + "RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None + ) + OIDC_RP_CLIENT_ID = values.Value( + "people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None + ) + OIDC_RP_CLIENT_SECRET = values.Value( + "ThisIsAnExampleKeyForDevPurposeOnly", + environ_name="OIDC_RP_CLIENT_SECRET", + environ_prefix=None, + ) + OIDC_OP_JWKS_ENDPOINT = values.Value( + environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None + ) + OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value( + environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None + ) + OIDC_OP_TOKEN_ENDPOINT = values.Value( + None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None + ) + OIDC_OP_USER_ENDPOINT = values.Value( + None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None + ) + OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue( + {}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None + ) + OIDC_RP_SCOPES = values.Value( + "openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None + ) + LOGIN_REDIRECT_URL = values.Value( + None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None + ) + LOGIN_REDIRECT_URL_FAILURE = values.Value( + None, environ_name="LOGIN_REDIRECT_URL_FAILURE", environ_prefix=None + ) + LOGOUT_REDIRECT_URL = values.Value( + None, environ_name="LOGOUT_REDIRECT_URL", environ_prefix=None + ) + OIDC_USE_NONCE = values.BooleanValue( + default=True, environ_name="OIDC_USE_NONCE", environ_prefix=None + ) + OIDC_REDIRECT_REQUIRE_HTTPS = values.BooleanValue( + default=False, environ_name="OIDC_REDIRECT_REQUIRE_HTTPS", environ_prefix=None + ) + OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue( + default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None + ) + # pylint: disable=invalid-name @property def ENVIRONMENT(self): @@ -381,7 +414,7 @@ class Development(Base): ALLOWED_HOSTS = ["*"] CORS_ALLOW_ALL_ORIGINS = True - CSRF_TRUSTED_ORIGINS = ["http://localhost:8072"] + CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"] DEBUG = True SESSION_COOKIE_NAME = "people_sessionid" @@ -396,10 +429,6 @@ class Development(Base): class Test(Base): """Test environment settings""" - SIMPLE_JWT = { - "USER_ID_FIELD": "sub", - "USER_ID_CLAIM": "sub", - } LOGGING = values.DictValue( { "version": 1, diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index d670d4a..8a74bc9 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -35,7 +35,6 @@ dependencies = [ "django-storages==1.14.2", "django-timezone-field>=5.1", "django==5.0.2", - "djangorestframework-simplejwt[crypto]==5.3.1", "djangorestframework==3.14.0", "drf_spectacular==0.27.1", "dockerflow==2024.1.0", @@ -50,6 +49,7 @@ dependencies = [ "sentry-sdk==1.40.4", "url-normalize==1.4.3", "whitenoise==6.6.0", + "mozilla-django-oidc==4.0.0", ] [project.urls]