diff --git a/src/dashboard/src/components/accounts/backends.py b/src/dashboard/src/components/accounts/backends.py
index 1e251cb7df..8be862ad7e 100644
--- a/src/dashboard/src/components/accounts/backends.py
+++ b/src/dashboard/src/components/accounts/backends.py
@@ -2,6 +2,7 @@
from components.helpers import generate_api_key
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
from django_auth_ldap.backend import LDAPBackend
from django_cas_ng.backends import CASBackend
from josepy.jws import JWS
@@ -42,6 +43,49 @@ class CustomOIDCBackend(OIDCAuthenticationBackend):
Provide OpenID Connect authentication
"""
+ def get_settings(self, attr, *args):
+ if attr in [
+ "OIDC_RP_CLIENT_ID",
+ "OIDC_RP_CLIENT_SECRET",
+ "OIDC_OP_AUTHORIZATION_ENDPOINT",
+ "OIDC_OP_TOKEN_ENDPOINT",
+ "OIDC_OP_USER_ENDPOINT",
+ "OIDC_OP_JWKS_ENDPOINT",
+ "OIDC_OP_LOGOUT_ENDPOINT",
+ ]:
+ # 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")
+ self.OIDC_OP_TOKEN_ENDPOINT = self.get_settings("OIDC_OP_TOKEN_ENDPOINT")
+ self.OIDC_OP_USER_ENDPOINT = self.get_settings("OIDC_OP_USER_ENDPOINT")
+ self.OIDC_OP_JWKS_ENDPOINT = self.get_settings("OIDC_OP_JWKS_ENDPOINT")
+
+ 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..e9e360fb99 100644
--- a/src/dashboard/src/components/accounts/urls.py
+++ b/src/dashboard/src/components/accounts/urls.py
@@ -37,6 +37,24 @@
path("logout/", django_cas_ng.views.LogoutView.as_view(), name="logout"),
]
+elif "mozilla_django_oidc" in settings.INSTALLED_APPS:
+ from components.accounts.views import CustomOIDCLogoutView
+
+ urlpatterns += [
+ path(
+ "login/",
+ django.contrib.auth.views.LoginView.as_view(
+ template_name="accounts/login.html"
+ ),
+ name="login",
+ ),
+ path(
+ "logout/",
+ CustomOIDCLogoutView.as_view(),
+ 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..00f81df19d 100644
--- a/src/dashboard/src/components/accounts/views.py
+++ b/src/dashboard/src/components/accounts/views.py
@@ -14,6 +14,8 @@
#
# You should have received a copy of the GNU General Public License
# along with Archivematica. If not, see .
+from urllib.parse import urlencode
+
import components.decorators as decorators
from components.accounts.forms import ApiKeyForm
from components.accounts.forms import UserChangeForm
@@ -24,6 +26,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 logout_then_login
+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 +35,8 @@
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 mozilla_django_oidc.views import OIDCLogoutView
from tastypie.models import ApiKey
@@ -193,3 +199,113 @@ def delete(request, id):
return redirect("accounts:accounts_index")
except Exception:
raise Http404
+
+
+class CustomOIDCAuthenticationRequestView(OIDCAuthenticationRequestView):
+ """
+ Provide OpenID Connect authentication
+ """
+
+ def get_settings(self, attr, *args):
+ if attr in [
+ "OIDC_RP_CLIENT_ID",
+ "OIDC_RP_CLIENT_SECRET",
+ "OIDC_OP_AUTHORIZATION_ENDPOINT",
+ "OIDC_OP_TOKEN_ENDPOINT",
+ "OIDC_OP_USER_ENDPOINT",
+ "OIDC_OP_JWKS_ENDPOINT",
+ "OIDC_OP_LOGOUT_ENDPOINT",
+ ]:
+ # 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
+ self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID")
+ self.OIDC_RP_CLIENT_SECRET = self.get_settings("OIDC_RP_CLIENT_SECRET")
+ self.OIDC_OP_AUTH_ENDPOINT = self.get_settings("OIDC_OP_AUTHORIZATION_ENDPOINT")
+
+ return super().get(request)
+
+
+class CustomOIDCLogoutView(OIDCLogoutView):
+ """
+ Provide OpenID Logout capability
+ """
+
+ def get(self, request):
+ self.request = request
+
+ if "oidc_id_token" in request.session:
+ # If the user authenticated via OIDC, perform the OIDC logout.
+ redirect = super().post(request)
+
+ if "providername" in request.session:
+ del request.session["providername"]
+
+ return redirect
+ else:
+ # If the user did not authenticate via OIDC, perform a local logout and redirect to login.
+ return logout_then_login(request)
+
+
+def get_oidc_logout_url(request):
+ """
+ Constructs the OIDC logout URL used in OIDCLogoutView.
+ """
+ # Retrieve the ID token from the session.
+ id_token = request.session.get("oidc_id_token")
+
+ if not id_token:
+ raise ValueError("ID token not found in session.")
+
+ # Get the end session endpoint.
+ end_session_endpoint = getattr(settings, "OIDC_OP_LOGOUT_ENDPOINT", None)
+
+ # Override the end session endpoint from the provider settings if available.
+ 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, {})
+ end_session_endpoint = provider_settings.get("OIDC_OP_LOGOUT_ENDPOINT")
+
+ if end_session_endpoint is None:
+ raise ImproperlyConfigured(
+ f"Setting OIDC_OP_LOGOUT_ENDPOINT for provider {provider_name} not found"
+ )
+
+ if not end_session_endpoint:
+ raise ValueError("OIDC logout endpoint not configured for provider.")
+
+ # Define the post logout redirect URL.
+ post_logout_redirect_uri = request.build_absolute_uri("/")
+
+ # Construct the logout URL with required parameters.
+ params = {
+ "id_token_hint": id_token,
+ "post_logout_redirect_uri": post_logout_redirect_uri,
+ }
+ logout_url = f"{end_session_endpoint}?{urlencode(params)}"
+
+ return logout_url
diff --git a/src/dashboard/src/middleware/common.py b/src/dashboard/src/middleware/common.py
index 0750ecc05b..9f5b6ec0e0 100644
--- a/src/dashboard/src/middleware/common.py
+++ b/src/dashboard/src/middleware/common.py
@@ -122,3 +122,23 @@ 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, ""
+ ).upper()
+
+ 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 67eaef1121..462cb75175 100644
--- a/src/dashboard/src/settings/base.py
+++ b/src/dashboard/src/settings/base.py
@@ -126,6 +126,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",
@@ -204,6 +209,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
@@ -629,11 +635,32 @@ 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")
+
+ INSTALLED_APPS += ["mozilla_django_oidc"]
ALLOW_USER_EDITS = False
+ OIDC_STORE_ID_TOKEN = True
+
+ OIDC_AUTHENTICATE_CLASS = (
+ "components.accounts.views.CustomOIDCAuthenticationRequestView"
+ )
AUTHENTICATION_BACKENDS += ["components.accounts.backends.CustomOIDCBackend"]
LOGIN_EXEMPT_URLS.append(r"^oidc")
- INSTALLED_APPS += ["mozilla_django_oidc"]
+
+ if not OIDC_ALLOW_LOCAL_AUTHENTICATION:
+ LOGIN_URL = "/oidc/authenticate/"
+ AUTHENTICATION_BACKENDS = [
+ backend
+ for backend in AUTHENTICATION_BACKENDS
+ if backend != "django.contrib.auth.backends.ModelBackend"
+ ]
+
+ # Insert OIDC before the redirect to LOGIN_URL
+ MIDDLEWARE.insert(
+ MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware") + 1,
+ "middleware.common.OidcCaptureQueryParamMiddleware",
+ )
from .components.oidc_auth import *
diff --git a/src/dashboard/src/settings/components/oidc_auth.py b/src/dashboard/src/settings/components/oidc_auth.py
index 94b8978145..1a15be2359 100644
--- a/src/dashboard/src/settings/components/oidc_auth.py
+++ b/src/dashboard/src/settings/components/oidc_auth.py
@@ -1,5 +1,41 @@
import os
+
+def get_oidc_secondary_providers(oidc_secondary_provider_names):
+ """Build secondary OIDC provider details dict. Takes a list of secondary
+ OIDC providers and gathers details about these providers from env vars.
+ Output dict contains details for each OIDC connection which can then be
+ referenced by name.
+ """
+
+ providers = {}
+
+ for provider_name in oidc_secondary_provider_names:
+ provider_name = provider_name.strip()
+ client_id = os.environ.get(f"OIDC_RP_CLIENT_ID_{provider_name}")
+ client_secret = os.environ.get(f"OIDC_RP_CLIENT_SECRET_{provider_name}")
+ authorization_endpoint = os.environ.get(
+ f"OIDC_OP_AUTHORIZATION_ENDPOINT_{provider_name}", ""
+ )
+ token_endpoint = os.environ.get(f"OIDC_OP_TOKEN_ENDPOINT_{provider_name}", "")
+ user_endpoint = os.environ.get(f"OIDC_OP_USER_ENDPOINT_{provider_name}", "")
+ jwks_endpoint = os.environ.get(f"OIDC_OP_JWKS_ENDPOINT_{provider_name}", "")
+ logout_endpoint = os.environ.get(f"OIDC_OP_LOGOUT_ENDPOINT_{provider_name}", "")
+
+ if client_id and client_secret:
+ providers[provider_name] = {
+ "OIDC_RP_CLIENT_ID": client_id,
+ "OIDC_RP_CLIENT_SECRET": client_secret,
+ "OIDC_OP_AUTHORIZATION_ENDPOINT": authorization_endpoint,
+ "OIDC_OP_TOKEN_ENDPOINT": token_endpoint,
+ "OIDC_OP_USER_ENDPOINT": user_endpoint,
+ "OIDC_OP_JWKS_ENDPOINT": jwks_endpoint,
+ "OIDC_OP_LOGOUT_ENDPOINT": logout_endpoint,
+ }
+
+ return providers
+
+
OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", "")
OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", "")
@@ -7,6 +43,7 @@
OIDC_OP_TOKEN_ENDPOINT = ""
OIDC_OP_USER_ENDPOINT = ""
OIDC_OP_JWKS_ENDPOINT = ""
+OIDC_OP_LOGOUT_ENDPOINT = ""
AZURE_TENANT_ID = os.environ.get("AZURE_TENANT_ID", "")
if AZURE_TENANT_ID:
@@ -23,10 +60,24 @@
"https://login.microsoftonline.com/%s/discovery/v2.0/keys" % AZURE_TENANT_ID
)
else:
- OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ["OIDC_OP_AUTHORIZATION_ENDPOINT"]
- OIDC_OP_TOKEN_ENDPOINT = os.environ["OIDC_OP_TOKEN_ENDPOINT"]
- OIDC_OP_USER_ENDPOINT = os.environ["OIDC_OP_USER_ENDPOINT"]
+ 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_OP_LOGOUT_ENDPOINT = os.environ.get("OIDC_OP_LOGOUT_ENDPOINT", "")
+
+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)
+
+if OIDC_OP_LOGOUT_ENDPOINT:
+ OIDC_OP_LOGOUT_URL_METHOD = "components.accounts.views.get_oidc_logout_url"
OIDC_RP_SIGN_ALGO = os.environ.get("OIDC_RP_SIGN_ALGO", "HS256")
diff --git a/src/dashboard/src/templates/layout.html b/src/dashboard/src/templates/layout.html
index 5eddb7e82c..9c05e46de1 100644
--- a/src/dashboard/src/templates/layout.html
+++ b/src/dashboard/src/templates/layout.html
@@ -19,7 +19,9 @@
-
+ {% if user.is_authenticated %}
+
+ {% endif %}
{% block js %}{% endblock %}