diff --git a/src/dashboard/src/components/accounts/backends.py b/src/dashboard/src/components/accounts/backends.py index 1e251cb7df..b72dd468b0 100644 --- a/src/dashboard/src/components/accounts/backends.py +++ b/src/dashboard/src/components/accounts/backends.py @@ -4,6 +4,7 @@ from django.conf import settings from django_auth_ldap.backend import LDAPBackend from django_cas_ng.backends import CASBackend +from django.core.exceptions import ImproperlyConfigured from josepy.jws import JWS from mozilla_django_oidc.auth import OIDCAuthenticationBackend from shibboleth.backends import ShibbolethRemoteUserBackend @@ -42,6 +43,38 @@ class CustomOIDCBackend(OIDCAuthenticationBackend): Provide OpenID Connect authentication """ + def get_settings(self, attr, *args): + if attr in ["OIDC_RP_CLIENT_ID", "OIDC_RP_CLIENT_SECRET"]: + # Retrieve the request object stored in the instance. + request = getattr(self, "request", None) + + if request: + provider_name = request.session.get("providername") + + if ( + provider_name + and provider_name in settings.OIDC_SECONDARY_PROVIDER_NAMES + ): + provider_settings = settings.OIDC_PROVIDERS.get(provider_name, {}) + value = provider_settings.get(attr) + + if value is None: + raise ImproperlyConfigured( + f"Setting {attr} for provider {provider_name} not found" + ) + return value + + # If request is None or provider_name session var is not set or attr is + # not in the list, call the superclass's get_settings method. + return OIDCAuthenticationBackend.get_settings(attr, *args) + + def authenticate(self, request, **kwargs): + self.request = request + self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID") + self.OIDC_RP_CLIENT_SECRET = self.get_settings("OIDC_RP_CLIENT_SECRET") + + return super().authenticate(request, **kwargs) + def get_userinfo(self, access_token, id_token, verified_id): """ Extract user details from JSON web tokens diff --git a/src/dashboard/src/components/accounts/urls.py b/src/dashboard/src/components/accounts/urls.py index 343fc20f11..40eb8a7848 100644 --- a/src/dashboard/src/components/accounts/urls.py +++ b/src/dashboard/src/components/accounts/urls.py @@ -37,6 +37,20 @@ path("logout/", django_cas_ng.views.LogoutView.as_view(), name="logout"), ] +elif "mozilla_django_oidc" in settings.INSTALLED_APPS: + from components.accounts.views import CustomOIDCLoginView + + urlpatterns += [ + path( + "login/", + CustomOIDCLoginView.as_view( + template_name="accounts/login.html" + ), + name="login", + ), + path("logout/", django.contrib.auth.views.logout_then_login, name="logout"), + ] + else: urlpatterns += [ path( diff --git a/src/dashboard/src/components/accounts/views.py b/src/dashboard/src/components/accounts/views.py index 7a06666715..9cc538fee9 100644 --- a/src/dashboard/src/components/accounts/views.py +++ b/src/dashboard/src/components/accounts/views.py @@ -24,6 +24,8 @@ from django.contrib import messages from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.models import User +from django.contrib.auth.views import LoginView +from django.core.exceptions import ImproperlyConfigured from django.http import Http404 from django.shortcuts import get_object_or_404 from django.shortcuts import redirect @@ -31,6 +33,7 @@ from django.urls import reverse from django.utils.translation import gettext as _ from main.models import UserProfile +from mozilla_django_oidc.views import OIDCAuthenticationRequestView from tastypie.models import ApiKey @@ -193,3 +196,50 @@ def delete(request, id): return redirect("accounts:accounts_index") except Exception: raise Http404 + + +class CustomOIDCLoginView(LoginView): + def get(self, request, *args, **kwargs): + if settings.OIDC_ALLOW_LOCAL_AUTHENTICATION: + return super().get(request, *args, **kwargs) + + login_url = reverse("oidc_authentication_init") + + # Redirect to the OIDC authentication URL with the provider set + return redirect(f"{login_url}") + + +class CustomOIDCAuthenticationRequestView(OIDCAuthenticationRequestView): + """ + Provide OpenID Connect authentication + """ + + def get_settings(self, attr, *args): + if attr in ["OIDC_RP_CLIENT_ID", "OIDC_RP_CLIENT_SECRET"]: + # Retrieve the request object stored in the instance. + request = getattr(self, "request", None) + + if request: + provider_name = request.session.get("providername") + + if ( + provider_name + and provider_name in settings.OIDC_SECONDARY_PROVIDER_NAMES + ): + provider_settings = settings.OIDC_PROVIDERS.get(provider_name, {}) + value = provider_settings.get(attr) + + if value is None: + raise ImproperlyConfigured( + f"Setting {attr} for provider {provider_name} not found" + ) + return value + + # If request is None or provider_name session var is not set or attr is + # not in the list, call the superclass's get_settings method. + return OIDCAuthenticationRequestView.get_settings(attr, *args) + + def get(self, request): + self.request = request + + return super().get(request) diff --git a/src/dashboard/src/middleware/common.py b/src/dashboard/src/middleware/common.py index 0750ecc05b..6698059dd1 100644 --- a/src/dashboard/src/middleware/common.py +++ b/src/dashboard/src/middleware/common.py @@ -21,6 +21,7 @@ import elasticsearch from django.conf import settings from django.http import HttpResponseServerError +from django.http import HttpResponseRedirect from django.shortcuts import render from django.template import TemplateDoesNotExist from django.utils.deprecation import MiddlewareMixin @@ -122,3 +123,21 @@ def make_profile(self, user, shib_meta): entitlements = shib_meta["entitlement"].split(";") user.is_superuser = settings.SHIBBOLETH_ADMIN_ENTITLEMENT in entitlements user.save() + + +class OidcCaptureQueryParamMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if not request.user.is_authenticated: + # Capture query parameter value and store it in the session. + provider_name = request.GET.get(settings.OIDC_PROVIDER_QUERY_PARAM_NAME) + + if provider_name and provider_name in settings.OIDC_PROVIDERS: + request.session["providername"] = provider_name + + # Continue processing the request. + response = self.get_response(request) + + return response diff --git a/src/dashboard/src/settings/base.py b/src/dashboard/src/settings/base.py index e971d30979..ed17e9873e 100644 --- a/src/dashboard/src/settings/base.py +++ b/src/dashboard/src/settings/base.py @@ -123,6 +123,11 @@ def _get_settings_from_file(path): "option": "oidc_authentication", "type": "boolean", }, + "oidc_allow_local_authentication": { + "section": "Dashboard", + "option": "oidc_allow_local_authentication", + "type": "boolean", + }, "storage_service_client_timeout": { "section": "Dashboard", "option": "storage_service_client_timeout", @@ -201,6 +206,7 @@ def _get_settings_from_file(path): csrf_trusted_origins = use_x_forwarded_host = False oidc_authentication = False +oidc_allow_local_authentication = True storage_service_client_timeout = 86400 storage_service_client_quick_timeout = 5 agentarchives_client_timeout = 300 @@ -626,8 +632,17 @@ def _get_settings_from_file(path): OIDC_AUTHENTICATION = config.get("oidc_authentication") if OIDC_AUTHENTICATION: + OIDC_ALLOW_LOCAL_AUTHENTICATION = config.get("oidc_allow_local_authentication") + + # Insert OIDC before the redirect to LOGIN_URL + MIDDLEWARE.insert( + MIDDLEWARE.index("installer.middleware.ConfigurationCheckMiddleware") - 1, + "middleware.common.OidcCaptureQueryParamMiddleware", + ) + ALLOW_USER_EDITS = False + OIDC_AUTHENTICATE_CLASS = "components.accounts.views.CustomOIDCAuthenticationRequestView" AUTHENTICATION_BACKENDS += ["components.accounts.backends.CustomOIDCBackend"] LOGIN_EXEMPT_URLS.append(r"^oidc") INSTALLED_APPS += ["mozilla_django_oidc"] diff --git a/src/dashboard/src/settings/components/oidc_auth.py b/src/dashboard/src/settings/components/oidc_auth.py index 94b8978145..71c60733da 100644 --- a/src/dashboard/src/settings/components/oidc_auth.py +++ b/src/dashboard/src/settings/components/oidc_auth.py @@ -1,5 +1,33 @@ import os + +def get_oidc_secondary_providers(oidc_secondary_provider_names): + providers = {} + + for provider_name in oidc_secondary_provider_names: + provider_name = provider_name.strip() + client_id = os.environ.get(f"OIDC_PROVIDER_CLIENT_ID_{provider_name.upper()}") + client_secret = os.environ.get( + f"OIDC_PROVIDER_CLIENT_SECRET_{provider_name.upper()}" + ) + + if client_id and client_secret: + providers[provider_name] = { + "OIDC_RP_CLIENT_ID": client_id, + "OIDC_RP_CLIENT_SECRET": client_secret, + } + + return providers + + +OIDC_SECONDARY_PROVIDER_NAMES = os.environ.get( + "OIDC_SECONDARY_PROVIDER_NAMES", "" +).split(",") +OIDC_PROVIDER_QUERY_PARAM_NAME = os.environ.get( + "OIDC_PROVIDER_QUERY_PARAM_NAME", "secondary" +) +OIDC_PROVIDERS = get_oidc_secondary_providers(OIDC_SECONDARY_PROVIDER_NAMES) + OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", "") OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", "")