From 2dac0b914bb45a670755516707f462894a77ee52 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Sun, 26 Jul 2026 08:18:53 +0200 Subject: [PATCH] Implement SOCIAL_AUTH_OKTA_OAUTH2 login. --- docs/setup/administrators/configuration.md | 17 +++++++++++++++++ .../users/includes/okta_login_button.html | 8 ++++++++ hypha/apply/users/templates/users/login.html | 3 +++ .../users/passwordless_login_signup.html | 3 +++ hypha/apply/users/templatetags/users_tags.py | 2 ++ hypha/apply/users/tests/test_oauth_access.py | 15 +++++++++++++++ hypha/apply/users/utils.py | 6 +++++- hypha/core/context_processors.py | 1 + hypha/settings/base.py | 13 +++++++++++++ hypha/settings/django.py | 1 + 10 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 hypha/apply/users/templates/users/includes/okta_login_button.html diff --git a/docs/setup/administrators/configuration.md b/docs/setup/administrators/configuration.md index f71d70f371..5365d20a64 100644 --- a/docs/setup/administrators/configuration.md +++ b/docs/setup/administrators/configuration.md @@ -219,6 +219,23 @@ Staff e-mail domain. Used for OAUTH2 whitelist default value and staff account c ---- +Google OAuth2 credentials. Create them at . When a key is set a "Log in with your email" button is shown on the login page. The whitelist defaults to `STAFF_EMAIL_DOMAINS`. + + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = env.str('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', '') + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = env.str('SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET', '') + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = env.list('SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS', STAFF_EMAIL_DOMAINS) + +---- + +Okta OAuth2 credentials. Create an OIDC "Web" application in the Okta admin console and grant it the `openid`, `profile` and `email` scopes. `SOCIAL_AUTH_OKTA_OAUTH2_API_URL` must point at the Okta authorization server, e.g. `https://your-org.okta.com/oauth2/default`. When a key is set a "Log in with Okta" button is shown on the login page. The whitelist defaults to `STAFF_EMAIL_DOMAINS`. + + SOCIAL_AUTH_OKTA_OAUTH2_KEY = env.str('SOCIAL_AUTH_OKTA_OAUTH2_KEY', '') + SOCIAL_AUTH_OKTA_OAUTH2_SECRET = env.str('SOCIAL_AUTH_OKTA_OAUTH2_SECRET', '') + SOCIAL_AUTH_OKTA_OAUTH2_API_URL = env.str('SOCIAL_AUTH_OKTA_OAUTH2_API_URL', '') + SOCIAL_AUTH_OKTA_OAUTH2_WHITELISTED_DOMAINS = env.list('SOCIAL_AUTH_OKTA_OAUTH2_WHITELISTED_DOMAINS', STAFF_EMAIL_DOMAINS) + +---- + Should staff identities be obscured from Applicants (ie. comments will be ORG_LONG_NAME rather than "John Doe"). HIDE_STAFF_IDENTITY = env.bool('HIDE_STAFF_IDENTITY', False) diff --git a/hypha/apply/users/templates/users/includes/okta_login_button.html b/hypha/apply/users/templates/users/includes/okta_login_button.html new file mode 100644 index 0000000000..14469a8fbe --- /dev/null +++ b/hypha/apply/users/templates/users/includes/okta_login_button.html @@ -0,0 +1,8 @@ +{% load i18n heroicons %} + + {% heroicon_mini "key" size=18 class="opacity-80" aria_hidden=true %} + {% trans "Log in with Okta" %} + diff --git a/hypha/apply/users/templates/users/login.html b/hypha/apply/users/templates/users/login.html index 50e38311cb..3d9bb06c8e 100644 --- a/hypha/apply/users/templates/users/login.html +++ b/hypha/apply/users/templates/users/login.html @@ -97,6 +97,9 @@

{% if GOOGLE_OAUTH2 %} {% include "users/includes/org_login_button.html" %} {% endif %} + {% if OKTA_OAUTH2 %} + {% include "users/includes/okta_login_button.html" %} + {% endif %} {% else %} diff --git a/hypha/apply/users/templates/users/passwordless_login_signup.html b/hypha/apply/users/templates/users/passwordless_login_signup.html index bb719da7ae..d5ef59528d 100644 --- a/hypha/apply/users/templates/users/passwordless_login_signup.html +++ b/hypha/apply/users/templates/users/passwordless_login_signup.html @@ -57,6 +57,9 @@

{% if GOOGLE_OAUTH2 %} {% include "users/includes/org_login_button.html" %} {% endif %} + {% if OKTA_OAUTH2 %} + {% include "users/includes/okta_login_button.html" %} + {% endif %} diff --git a/hypha/apply/users/templatetags/users_tags.py b/hypha/apply/users/templatetags/users_tags.py index 32143aa3dd..aae81b8951 100644 --- a/hypha/apply/users/templatetags/users_tags.py +++ b/hypha/apply/users/templatetags/users_tags.py @@ -21,6 +21,8 @@ def backend_name(name): "google-oauth": "Google OAuth", "google-oauth2": "Google OAuth", "google-openidconnect": "Google OpenId", + "okta-oauth2": "Okta OAuth", + "okta-openidconnect": "Okta OpenId", }.get(name, name) diff --git a/hypha/apply/users/tests/test_oauth_access.py b/hypha/apply/users/tests/test_oauth_access.py index 6da73c5000..55c9a99c72 100644 --- a/hypha/apply/users/tests/test_oauth_access.py +++ b/hypha/apply/users/tests/test_oauth_access.py @@ -58,3 +58,18 @@ def test_oauth_whitelisted_user_can_access_oauth_settings_page(self): self.assertNotContains(response, "Disconnect Google OAuth") self.assertTemplateUsed(response, "users/oauth.html") + + @override_settings( + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS=[], + SOCIAL_AUTH_OKTA_OAUTH2_WHITELISTED_DOMAINS=["email.com"], + ) + def test_oauth_okta_whitelisted_user_can_access_oauth_settings_page(self): + """ + Checks that a user whose email is whitelisted for Okta (but not Google) + can still access the OAuth page + """ + self.login() + + response = self.client.get(reverse("users:oauth"), follow=True) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "users/oauth.html") diff --git a/hypha/apply/users/utils.py b/hypha/apply/users/utils.py index a3271a6474..779f9f25bb 100644 --- a/hypha/apply/users/utils.py +++ b/hypha/apply/users/utils.py @@ -52,7 +52,11 @@ def can_use_oauth_check(user): """ try: domain = user.email.split("@")[-1] - return domain in settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS + whitelisted_domains = { + *settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS, + *settings.SOCIAL_AUTH_OKTA_OAUTH2_WHITELISTED_DOMAINS, + } + return domain in whitelisted_domains except AttributeError: # Anonymous user or setting not defined pass diff --git a/hypha/core/context_processors.py b/hypha/core/context_processors.py index ab53461b44..46b70f49ad 100644 --- a/hypha/core/context_processors.py +++ b/hypha/core/context_processors.py @@ -15,6 +15,7 @@ def global_vars(request): "HIDE_STAFF_IDENTITY": settings.HIDE_STAFF_IDENTITY, "HIDE_IDENTITY_FROM_REVIEWERS": settings.HIDE_IDENTITY_FROM_REVIEWERS, "GOOGLE_OAUTH2": settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, + "OKTA_OAUTH2": settings.SOCIAL_AUTH_OKTA_OAUTH2_KEY, "ENABLE_PUBLIC_SIGNUP": settings.ENABLE_PUBLIC_SIGNUP, "PASSKEYS_ENABLED": passkeys_enabled(), "SENTRY_TRACES_SAMPLE_RATE": settings.SENTRY_TRACES_SAMPLE_RATE, diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 9bdf4d2960..0063116267 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -441,6 +441,19 @@ SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = env.str("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "") SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = env.str("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "") +# Set the Okta OAuth2 credentials in ENV variables or local.py +# Create an OIDC "Web" app in the Okta admin console and grant it the +# openid, profile and email scopes. +# SOCIAL_AUTH_OKTA_OAUTH2_API_URL should point at the Okta authorization +# server, e.g. https://your-org.okta.com/oauth2/default +SOCIAL_AUTH_OKTA_OAUTH2_WHITELISTED_DOMAINS = env.list( + "SOCIAL_AUTH_OKTA_OAUTH2_WHITELISTED_DOMAINS", STAFF_EMAIL_DOMAINS +) + +SOCIAL_AUTH_OKTA_OAUTH2_KEY = env.str("SOCIAL_AUTH_OKTA_OAUTH2_KEY", "") +SOCIAL_AUTH_OKTA_OAUTH2_SECRET = env.str("SOCIAL_AUTH_OKTA_OAUTH2_SECRET", "") +SOCIAL_AUTH_OKTA_OAUTH2_API_URL = env.str("SOCIAL_AUTH_OKTA_OAUTH2_API_URL", "") + SOCIAL_AUTH_URL_NAMESPACE = "social" SOCIAL_AUTH_LOGIN_ERROR_URL = "users:login" SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = "users:account" diff --git a/hypha/settings/django.py b/hypha/settings/django.py index d32dff0f3b..a5b0b11ccb 100644 --- a/hypha/settings/django.py +++ b/hypha/settings/django.py @@ -214,6 +214,7 @@ AUTHENTICATION_BACKENDS = ( "social_core.backends.google.GoogleOAuth2", + "social_core.backends.okta.OktaOAuth2", CUSTOM_AUTH_BACKEND, )