Skip to content

Commit

Permalink
Add OIDC authentication flow enhancements
Browse files Browse the repository at this point in the history
Conditionally allow local AM authentication. Allow local AM
authentication by default.

Add ability to define more than one OIDC provider and select it using
request query params.
  • Loading branch information
sbreker committed Aug 1, 2024
1 parent bf0f70f commit 9316f78
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/dashboard/src/components/accounts/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/dashboard/src/components/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
50 changes: 50 additions & 0 deletions src/dashboard/src/components/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@
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
from django.shortcuts import render
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


Expand Down Expand Up @@ -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)
19 changes: 19 additions & 0 deletions src/dashboard/src/middleware/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
15 changes: 15 additions & 0 deletions src/dashboard/src/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down
28 changes: 28 additions & 0 deletions src/dashboard/src/settings/components/oidc_auth.py
Original file line number Diff line number Diff line change
@@ -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", "")

Expand Down

0 comments on commit 9316f78

Please sign in to comment.