diff --git a/docs/installation.md b/docs/installation.md index 57c34c39..84336f43 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -287,6 +287,7 @@ These are the environmental options available on meet backend. | OIDC_OP_AUTHORIZATION_ENDPOINT | oidc endpoint for authorization | | | OIDC_OP_TOKEN_ENDPOINT | oidc endpoint for token | | | OIDC_OP_USER_ENDPOINT | oidc endpoint for user | | +| OIDC_OP_USER_ENDPOINT_FORMAT | oidc endpoint format (AUTO, JWT, JSON) | AUTO | | OIDC_OP_LOGOUT_ENDPOINT | oidc endpoint for logout | | | OIDC_AUTH_REQUEST_EXTRA_PARAMS | extra parameters for oidc request | | | OIDC_RP_SCOPES | oidc scopes | openid email | @@ -301,6 +302,7 @@ These are the environmental options available on meet backend. | OIDC_REDIRECT_FIELD_NAME | direct field for oidc | returnTo | | OIDC_USERINFO_FULLNAME_FIELDS | full name claim from OIDC token | ["given_name", "usual_name"] | | OIDC_USERINFO_SHORTNAME_FIELD | shortname claim from OIDC token | given_name | +| OIDC_USERINFO_ESSENTIAL_CLAIMS | required claims from OIDC token | [] | | LIVEKIT_API_KEY | livekit api key | | | LIVEKIT_API_SECRET | livekit api secret | | | LIVEKIT_API_URL | livekit api url | | diff --git a/src/backend/core/authentication/backends.py b/src/backend/core/authentication/backends.py index 2f8a3866..fa183af3 100644 --- a/src/backend/core/authentication/backends.py +++ b/src/backend/core/authentication/backends.py @@ -1,12 +1,13 @@ """Authentication Backends for the Meet core app.""" +import contextlib + from django.conf import settings from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.utils.translation import gettext_lazy as _ -import requests -from mozilla_django_oidc.auth import ( - OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend, +from lasuite.oidc_login.backends import ( + OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend, ) from core.models import User @@ -17,93 +18,46 @@ from core.services.marketing import ( ) -class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend): +class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend): """Custom OpenID Connect (OIDC) Authentication Backend. 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. + def get_extra_claims(self, user_info): + """ + Return extra claims from user_info. - 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. + Args: + user_info (dict): The user information dictionary. Returns: - - dict: User details dictionary obtained from the OpenID Connect user endpoint. + dict: A dictionary of extra claims. + """ - - 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. - """ - - user_info = self.get_userinfo(access_token, id_token, payload) - sub = user_info.get("sub") - - if not sub: - raise SuspiciousOperation( - _("User info contained no recognizable user identification") - ) - - email = user_info.get("email") - user = self.get_existing_user(sub, email) - - claims = { - "email": email, + return { + # Get user's full name from OIDC fields defined in settings "full_name": self.compute_full_name(user_info), "short_name": user_info.get(settings.OIDC_USERINFO_SHORTNAME_FIELD), } - if not user and self.get_settings("OIDC_CREATE_USER", True): - user = User.objects.create( - sub=sub, - password="!", # noqa: S106 - **claims, - ) - if settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL: - self.signup_to_marketing_email(email) + def post_get_or_create_user(self, user, claims, is_new_user): + """ + Post-processing after user creation or retrieval. - elif not user: - return None + Args: + user (User): The user instance. + claims (dict): The claims dictionary. + is_new_user (bool): Indicates if the user was newly created. - if not user.is_active: - raise SuspiciousOperation(_("User account is disabled")) + Returns: + - None - self.update_user_if_needed(user, claims) - - return user + """ + email = claims["email"] + if is_new_user and email and settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL: + self.signup_to_marketing_email(email) @staticmethod def signup_to_marketing_email(email): @@ -116,7 +70,9 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend): Note: For a more robust solution, consider using Async task processing (Celery/Django-Q) """ - try: + with contextlib.suppress( + ContactCreationError, ImproperlyConfigured, ImportError + ): marketing_service = get_marketing_service() contact_data = ContactData( email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]} @@ -124,8 +80,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend): marketing_service.create_contact( contact_data, timeout=settings.BREVO_API_TIMEOUT ) - except (ContactCreationError, ImproperlyConfigured, ImportError): - pass def get_existing_user(self, sub, email): """Fetch existing user by sub or email.""" @@ -142,32 +96,3 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend): _("Multiple user accounts share a common email.") ) from e return None - - @staticmethod - def compute_full_name(user_info): - """Compute user's full name based on OIDC fields in settings.""" - full_name = " ".join( - filter( - None, - ( - user_info.get(field) - for field in settings.OIDC_USERINFO_FULLNAME_FIELDS - ), - ) - ) - return full_name or None - - @staticmethod - def update_user_if_needed(user, claims): - """Update user claims if they have changed.""" - user_fields = vars(user.__class__) # Get available model fields - updated_claims = { - key: value - for key, value in claims.items() - if value and key in user_fields and value != getattr(user, key) - } - - if not updated_claims: - return - - User.objects.filter(sub=user.sub).update(**updated_claims) diff --git a/src/backend/core/authentication/urls.py b/src/backend/core/authentication/urls.py deleted file mode 100644 index 2a66c83d..00000000 --- a/src/backend/core/authentication/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Authentication URLs for the People core app.""" - -from django.urls import path - -from mozilla_django_oidc.urls import urlpatterns as mozzila_oidc_urls - -from .views import OIDCLogoutCallbackView, OIDCLogoutView - -urlpatterns = [ - # Override the default 'logout/' path from Mozilla Django OIDC with our custom view. - path("logout/", OIDCLogoutView.as_view(), name="oidc_logout_custom"), - path( - "logout-callback/", - OIDCLogoutCallbackView.as_view(), - name="oidc_logout_callback", - ), - *mozzila_oidc_urls, -] diff --git a/src/backend/core/authentication/views.py b/src/backend/core/authentication/views.py deleted file mode 100644 index dcd2043b..00000000 --- a/src/backend/core/authentication/views.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Authentication Views for the People core app.""" - -import copy -from urllib.parse import urlencode - -from django.contrib import auth -from django.core.exceptions import SuspiciousOperation -from django.http import HttpResponseRedirect -from django.urls import reverse -from django.utils import crypto - -from mozilla_django_oidc.utils import ( - absolutify, -) -from mozilla_django_oidc.views import ( - OIDCAuthenticationCallbackView as MozillaOIDCAuthenticationCallbackView, -) -from mozilla_django_oidc.views import ( - OIDCAuthenticationRequestView as MozillaOIDCAuthenticationRequestView, -) -from mozilla_django_oidc.views import ( - OIDCLogoutView as MozillaOIDCOIDCLogoutView, -) - - -class OIDCLogoutView(MozillaOIDCOIDCLogoutView): - """Custom logout view for handling OpenID Connect (OIDC) logout flow. - - Adds support for handling logout callbacks from the identity provider (OP) - by initiating the logout flow if the user has an active session. - - The Django session is retained during the logout process to persist the 'state' OIDC parameter. - This parameter is crucial for maintaining the integrity of the logout flow between this call - and the subsequent callback. - """ - - @staticmethod - def persist_state(request, state): - """Persist the given 'state' parameter in the session's 'oidc_states' dictionary - - This method is used to store the OIDC state parameter in the session, according to the - structure expected by Mozilla Django OIDC's 'add_state_and_verifier_and_nonce_to_session' - utility function. - """ - - if "oidc_states" not in request.session or not isinstance( - request.session["oidc_states"], dict - ): - request.session["oidc_states"] = {} - - request.session["oidc_states"][state] = {} - request.session.save() - - def construct_oidc_logout_url(self, request): - """Create the redirect URL for interfacing with the OIDC provider. - - Retrieves the necessary parameters from the session and constructs the URL - required to initiate logout with the OpenID Connect provider. - - If no ID token is found in the session, the logout flow will not be initiated, - and the method will return the default redirect URL. - - The 'state' parameter is generated randomly and persisted in the session to ensure - its integrity during the subsequent callback. - """ - - oidc_logout_endpoint = self.get_settings("OIDC_OP_LOGOUT_ENDPOINT") - - if not oidc_logout_endpoint: - return self.redirect_url - - reverse_url = reverse("oidc_logout_callback") - id_token = request.session.get("oidc_id_token", None) - - if not id_token: - return self.redirect_url - - query = { - "id_token_hint": id_token, - "state": crypto.get_random_string(self.get_settings("OIDC_STATE_SIZE", 32)), - "post_logout_redirect_uri": absolutify(request, reverse_url), - } - - self.persist_state(request, query["state"]) - - return f"{oidc_logout_endpoint}?{urlencode(query)}" - - def post(self, request): - """Handle user logout. - - If the user is not authenticated, redirects to the default logout URL. - Otherwise, constructs the OIDC logout URL and redirects the user to start - the logout process. - - If the user is redirected to the default logout URL, ensure her Django session - is terminated. - """ - - logout_url = self.redirect_url - - if request.user.is_authenticated: - logout_url = self.construct_oidc_logout_url(request) - - # If the user is not redirected to the OIDC provider, ensure logout - if logout_url == self.redirect_url: - auth.logout(request) - - return HttpResponseRedirect(logout_url) - - -class OIDCLogoutCallbackView(MozillaOIDCOIDCLogoutView): - """Custom view for handling the logout callback from the OpenID Connect (OIDC) provider. - - Handles the callback after logout from the identity provider (OP). - Verifies the state parameter and performs necessary logout actions. - - The Django session is maintained during the logout process to ensure the integrity - of the logout flow initiated in the previous step. - """ - - http_method_names = ["get"] - - def get(self, request): - """Handle the logout callback. - - If the user is not authenticated, redirects to the default logout URL. - Otherwise, verifies the state parameter and performs necessary logout actions. - """ - - if not request.user.is_authenticated: - return HttpResponseRedirect(self.redirect_url) - - state = request.GET.get("state") - - if state not in request.session.get("oidc_states", {}): - msg = "OIDC callback state not found in session `oidc_states`!" - raise SuspiciousOperation(msg) - - del request.session["oidc_states"][state] - request.session.save() - - auth.logout(request) - - return HttpResponseRedirect(self.redirect_url) - - -class OIDCAuthenticationCallbackView(MozillaOIDCAuthenticationCallbackView): - """Custom callback view for handling the silent login flow.""" - - @property - def failure_url(self): - """Override the failure URL property to handle silent login flow - - A silent login failure (e.g., no active user session) should not be - considered as an authentication failure. - """ - if self.request.session.get("silent", None): - del self.request.session["silent"] - self.request.session.save() - return self.success_url - return super().failure_url - - -class OIDCAuthenticationRequestView(MozillaOIDCAuthenticationRequestView): - """Custom authentication view for handling the silent login flow.""" - - def get_extra_params(self, request): - """Handle 'prompt' extra parameter for the silent login flow - - This extra parameter is necessary to distinguish between a standard - authentication flow and the silent login flow. - """ - extra_params = self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", None) - if extra_params is None: - extra_params = {} - if request.GET.get("silent") == "true": - extra_params = copy.deepcopy(extra_params) - extra_params.update({"prompt": "none"}) - request.session["silent"] = True - request.session.save() - return extra_params diff --git a/src/backend/core/tests/authentication/test_backends.py b/src/backend/core/tests/authentication/test_backends.py index 576f9c0d..9315bbb4 100644 --- a/src/backend/core/tests/authentication/test_backends.py +++ b/src/backend/core/tests/authentication/test_backends.py @@ -58,7 +58,7 @@ def test_authentication_getter_new_user_no_email(monkeypatch): assert user.sub == "123" assert user.email is None - assert user.password == "!" + assert user.has_usable_password() is False assert models.User.objects.count() == 1 @@ -84,7 +84,7 @@ def test_authentication_getter_new_user_with_email(monkeypatch): assert user.email == email assert user.full_name is None assert user.short_name is None - assert user.password == "!" + assert user.has_usable_password() is False assert models.User.objects.count() == 1 @@ -110,7 +110,7 @@ def test_authentication_getter_new_user_with_names(monkeypatch, email): assert user.email == email assert user.full_name == "John Doe" assert user.short_name == "John" - assert user.password == "!" + assert user.has_usable_password() is False assert models.User.objects.count() == 1 @@ -129,7 +129,7 @@ def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkey django_assert_num_queries(0), pytest.raises( SuspiciousOperation, - match="User info contained no recognizable user identification", + match="Claims verification failed", ), ): klass.get_or_create_user(access_token="test-token", id_token=None, payload=None) diff --git a/src/backend/core/tests/authentication/test_urls.py b/src/backend/core/tests/authentication/test_urls.py deleted file mode 100644 index 0e20aac4..00000000 --- a/src/backend/core/tests/authentication/test_urls.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Unit tests for the Authentication URLs.""" - -from core.authentication.urls import urlpatterns - - -def test_urls_override_default_mozilla_django_oidc(): - """Custom URL patterns should override default ones from Mozilla Django OIDC.""" - - url_names = [u.name for u in urlpatterns] - assert url_names.index("oidc_logout_custom") < url_names.index("oidc_logout") diff --git a/src/backend/core/tests/authentication/test_views.py b/src/backend/core/tests/authentication/test_views.py deleted file mode 100644 index 20a124df..00000000 --- a/src/backend/core/tests/authentication/test_views.py +++ /dev/null @@ -1,359 +0,0 @@ -"""Unit tests for the Authentication Views.""" - -from unittest import mock -from urllib.parse import parse_qs, urlparse - -from django.contrib.auth.models import AnonymousUser -from django.contrib.sessions.middleware import SessionMiddleware -from django.core.exceptions import SuspiciousOperation -from django.test import RequestFactory -from django.test.utils import override_settings -from django.urls import reverse -from django.utils import crypto - -import pytest -from rest_framework.test import APIClient - -from core import factories -from core.authentication.views import ( - MozillaOIDCAuthenticationCallbackView, - OIDCAuthenticationCallbackView, - OIDCAuthenticationRequestView, - OIDCLogoutCallbackView, - OIDCLogoutView, -) - -pytestmark = pytest.mark.django_db - - -@override_settings(LOGOUT_REDIRECT_URL="/example-logout") -def test_view_logout_anonymous(): - """Anonymous users calling the logout url, - should be redirected to the specified LOGOUT_REDIRECT_URL.""" - - url = reverse("oidc_logout_custom") - response = APIClient().get(url) - - assert response.status_code == 302 - assert response.url == "/example-logout" - - -@mock.patch.object( - OIDCLogoutView, "construct_oidc_logout_url", return_value="/example-logout" -) -def test_view_logout(mocked_oidc_logout_url): - """Authenticated users should be redirected to OIDC provider for logout.""" - - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - url = reverse("oidc_logout_custom") - response = client.get(url) - - mocked_oidc_logout_url.assert_called_once() - - assert response.status_code == 302 - assert response.url == "/example-logout" - - -@override_settings(LOGOUT_REDIRECT_URL="/default-redirect-logout") -@mock.patch.object( - OIDCLogoutView, "construct_oidc_logout_url", return_value="/default-redirect-logout" -) -def test_view_logout_no_oidc_provider(mocked_oidc_logout_url): - """Authenticated users should be logged out when no OIDC provider is available.""" - - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - url = reverse("oidc_logout_custom") - - with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout: - response = client.get(url) - mocked_oidc_logout_url.assert_called_once() - mock_logout.assert_called_once() - - assert response.status_code == 302 - assert response.url == "/default-redirect-logout" - - -@override_settings(LOGOUT_REDIRECT_URL="/example-logout") -def test_view_logout_callback_anonymous(): - """Anonymous users calling the logout callback url, - should be redirected to the specified LOGOUT_REDIRECT_URL.""" - - url = reverse("oidc_logout_callback") - response = APIClient().get(url) - - assert response.status_code == 302 - assert response.url == "/example-logout" - - -@pytest.mark.parametrize( - "initial_oidc_states", - [{}, {"other_state": "foo"}], -) -def test_view_logout_persist_state(initial_oidc_states): - """State value should be persisted in session's data.""" - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - if initial_oidc_states: - request.session["oidc_states"] = initial_oidc_states - request.session.save() - - mocked_state = "mock_state" - - OIDCLogoutView().persist_state(request, mocked_state) - - assert "oidc_states" in request.session - assert request.session["oidc_states"] == { - "mock_state": {}, - **initial_oidc_states, - } - - -@override_settings(OIDC_OP_LOGOUT_ENDPOINT="/example-logout") -@mock.patch.object(OIDCLogoutView, "persist_state") -@mock.patch.object(crypto, "get_random_string", return_value="mocked_state") -def test_view_logout_construct_oidc_logout_url( - mocked_get_random_string, mocked_persist_state -): - """Should construct the logout URL to initiate the logout flow with the OIDC provider.""" - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - request.session["oidc_id_token"] = "mocked_oidc_id_token" - request.session.save() - - redirect_url = OIDCLogoutView().construct_oidc_logout_url(request) - - mocked_persist_state.assert_called_once() - mocked_get_random_string.assert_called_once() - - params = parse_qs(urlparse(redirect_url).query) - - assert params["id_token_hint"][0] == "mocked_oidc_id_token" - assert params["state"][0] == "mocked_state" - - url = reverse("oidc_logout_callback") - assert url in params["post_logout_redirect_uri"][0] - - -@override_settings(LOGOUT_REDIRECT_URL="/") -def test_view_logout_construct_oidc_logout_url_none_id_token(): - """If no ID token is available in the session, - the user should be redirected to the final URL.""" - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - redirect_url = OIDCLogoutView().construct_oidc_logout_url(request) - - assert redirect_url == "/" - - -@pytest.mark.parametrize( - "initial_state", - [None, {"other_state": "foo"}], -) -def test_view_logout_callback_wrong_state(initial_state): - """Should raise an error if OIDC state doesn't match session data.""" - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - if initial_state: - request.session["oidc_states"] = initial_state - request.session.save() - - callback_view = OIDCLogoutCallbackView.as_view() - - with pytest.raises(SuspiciousOperation) as excinfo: - callback_view(request) - - assert ( - str(excinfo.value) == "OIDC callback state not found in session `oidc_states`!" - ) - - -@override_settings(LOGOUT_REDIRECT_URL="/example-logout") -def test_view_logout_callback(): - """If state matches, callback should clear OIDC state and redirects.""" - - user = factories.UserFactory() - - request = RequestFactory().get("/logout-callback/", data={"state": "mocked_state"}) - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - mocked_state = "mocked_state" - - request.session["oidc_states"] = {mocked_state: {}} - request.session.save() - - callback_view = OIDCLogoutCallbackView.as_view() - - with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout: - - def clear_user(request): - # Assert state is cleared prior to logout - assert request.session["oidc_states"] == {} - request.user = AnonymousUser() - - mock_logout.side_effect = clear_user - response = callback_view(request) - mock_logout.assert_called_once() - - assert response.status_code == 302 - assert response.url == "/example-logout" - - -@pytest.mark.parametrize("mocked_extra_params_setting", [{"foo": 123}, {}, None]) -def test_view_authentication_default(settings, mocked_extra_params_setting): - """By default, authentication request should not trigger silent login.""" - - settings.OIDC_AUTH_REQUEST_EXTRA_PARAMS = mocked_extra_params_setting - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - request.GET = {} - - view = OIDCAuthenticationRequestView() - extra_params = view.get_extra_params(request) - - assert extra_params == (mocked_extra_params_setting or {}) - - -@pytest.mark.parametrize("mocked_extra_params_setting", [{"foo": 123}, {}, None]) -def test_view_authentication_silent_false(settings, mocked_extra_params_setting): - """Ensure setting 'silent' parameter to a random value doesn't trigger the silent login flow.""" - - settings.OIDC_AUTH_REQUEST_EXTRA_PARAMS = mocked_extra_params_setting - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - request.GET = {"silent": "foo"} - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - view = OIDCAuthenticationRequestView() - extra_params = view.get_extra_params(request) - - assert extra_params == (mocked_extra_params_setting or {}) - assert not request.session.get("silent") - - -@pytest.mark.parametrize("mocked_extra_params_setting", [{"foo": 123}, {}, None]) -def test_view_authentication_silent_true(settings, mocked_extra_params_setting): - """If 'silent' parameter is set to True, the silent login should be triggered.""" - settings.OIDC_AUTH_REQUEST_EXTRA_PARAMS = mocked_extra_params_setting - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - request.GET = {"silent": "true"} - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - view = OIDCAuthenticationRequestView() - extra_params = view.get_extra_params(request) - expected_params = {"prompt": "none"} - - assert ( - extra_params == {**mocked_extra_params_setting, **expected_params} - if mocked_extra_params_setting - else expected_params - ) - assert request.session.get("silent") is True - - -@mock.patch.object( - MozillaOIDCAuthenticationCallbackView, - "failure_url", - new_callable=mock.PropertyMock, - return_value="foo", -) -def test_view_callback_failure_url(mocked_failure_url): - """Test default behavior of the 'failure_url' property""" - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - view = OIDCAuthenticationCallbackView() - view.request = request - - returned_url = view.failure_url - - mocked_failure_url.assert_called_once() - assert returned_url == "foo" - - -@mock.patch.object( - OIDCAuthenticationCallbackView, - "success_url", - new_callable=mock.PropertyMock, - return_value="foo", -) -def test_view_callback_failure_url_silent_login(mocked_success_url): - """If a silent login was initiated and failed, it should not be treated as a failure.""" - - user = factories.UserFactory() - - request = RequestFactory().request() - request.user = user - - middleware = SessionMiddleware(get_response=lambda x: x) - middleware.process_request(request) - - request.session["silent"] = True - request.session.save() - - view = OIDCAuthenticationCallbackView() - view.request = request - - returned_url = view.failure_url - - mocked_success_url.assert_called_once() - assert returned_url == "foo" - assert not request.session.get("silent") diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index 88a33811..98969f18 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -3,10 +3,10 @@ from django.conf import settings from django.urls import include, path +from lasuite.oidc_login.urls import urlpatterns as oidc_urls from rest_framework.routers import DefaultRouter from core.api import get_frontend_configuration, viewsets -from core.authentication.urls import urlpatterns as oidc_urls # - Main endpoints router = DefaultRouter() diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 2c472180..acd04609 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -359,8 +359,8 @@ class Base(Configuration): SESSION_COOKIE_AGE = 60 * 60 * 12 # OIDC - Authorization Code Flow - OIDC_AUTHENTICATE_CLASS = "core.authentication.views.OIDCAuthenticationRequestView" - OIDC_CALLBACK_CLASS = "core.authentication.views.OIDCAuthenticationCallbackView" + OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView" + OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView" OIDC_CREATE_USER = values.BooleanValue( default=True, environ_name="OIDC_CREATE_USER", environ_prefix=None ) @@ -394,6 +394,9 @@ class Base(Configuration): OIDC_OP_USER_ENDPOINT = values.Value( None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None ) + OIDC_OP_USER_ENDPOINT_FORMAT = values.Value( + "AUTO", environ_name="OIDC_OP_USER_ENDPOINT_FORMAT", environ_prefix=None + ) OIDC_OP_LOGOUT_ENDPOINT = values.Value( None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None ) @@ -440,6 +443,11 @@ class Base(Configuration): environ_name="OIDC_USERINFO_SHORTNAME_FIELD", environ_prefix=None, ) + OIDC_USERINFO_ESSENTIAL_CLAIMS = values.ListValue( + default=[], + environ_name="OIDC_USERINFO_ESSENTIAL_CLAIMS", + environ_prefix=None, + ) # Video conference configuration LIVEKIT_CONFIGURATION = { diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 7f9b5779..38df6ac5 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "django-configurations==2.5.1", "django-cors-headers==4.7.0", "django-countries==7.6.1", + "django-lasuite==0.0.7", "django-parler==2.3", "redis==5.2.1", "django-redis==5.4.0",