From caf8839eddec2a5c76ecdbc1a9583dbe2ef17691 Mon Sep 17 00:00:00 2001 From: Eric Meehan Date: Fri, 23 Oct 2026 17:23:29 -0400 Subject: [PATCH] Dex --- .env.example | 3 ++- membership/config/settings.py | 4 +++- membership/members/tests/test_oidc.py | 29 ++++++++++++++++++++++++++- membership/members/views.py | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 0039863..eb6940a 100755 --- a/.env.example +++ b/.env.example @@ -13,7 +13,8 @@ POSTGRES_HOST=membership-db # Compose's local database is on an isolated network. For a remote database use # verify-full and mount its trusted root certificate via PGSSLROOTCERT. POSTGRES_SSLMODE=prefer -DEX_ISSUER=https://dex.eom.dev +# Must match discovery's issuer exactly, including its trailing slash. +DEX_ISSUER=https://dex.eom.dev/ DEX_CLIENT_ID=www-membership DEX_CLIENT_SECRET= DEX_CONNECTOR_ID=www-ldap diff --git a/membership/config/settings.py b/membership/config/settings.py index 47412d2..eddc4fd 100755 --- a/membership/config/settings.py +++ b/membership/config/settings.py @@ -77,7 +77,9 @@ EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1" EMAIL_TIMEOUT = 10 DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "accounts@eom.dev") -DEX_ISSUER = os.environ.get("DEX_ISSUER", "https://dex.eom.dev").rstrip("/") +# OIDC issuer identifiers are exact strings, including a trailing slash. +# Never normalize this value: discovery and ID-token iss must match it exactly. +DEX_ISSUER = os.environ.get("DEX_ISSUER", "https://dex.eom.dev/") if urlsplit(DEX_ISSUER).scheme != "https" or not urlsplit(DEX_ISSUER).hostname: raise ImproperlyConfigured("DEX_ISSUER must use HTTPS.") DEX_CLIENT_ID = os.environ.get("DEX_CLIENT_ID", "www-membership") diff --git a/membership/members/tests/test_oidc.py b/membership/members/tests/test_oidc.py index 5dc5497..c870ad6 100755 --- a/membership/members/tests/test_oidc.py +++ b/membership/members/tests/test_oidc.py @@ -6,7 +6,7 @@ from urllib.parse import parse_qs, urlsplit from authlib.integrations.django_client import OAuth from django.conf import settings from django.contrib.sessions.backends.db import SessionStore -from django.test import RequestFactory, TestCase +from django.test import RequestFactory, TestCase, override_settings from joserfc import jwt from joserfc.jwk import RSAKey @@ -71,3 +71,30 @@ class AuthlibFlowTests(TestCase): token["id_token"] = jwt.encode({"alg": "RS256", "kid": "test-key"}, claims, other) with self.assertRaises(Exception): self.exchange(remote, callback, token) + + +class DiscoveryIssuerTests(TestCase): + def test_settings_preserve_exact_configured_issuer(self): + import importlib + import os + + settings_module = importlib.import_module("config.settings") + try: + with patch.dict(os.environ, {"DEX_ISSUER": "https://dex.example.org/"}): + importlib.reload(settings_module) + self.assertEqual(settings_module.DEX_ISSUER, "https://dex.example.org/") + finally: + importlib.reload(settings_module) + + @override_settings(DEX_ISSUER="https://dex.example.org/") + @patch("members.views.OAuth") + def test_discovery_uses_single_slash_and_exact_issuer(self, oauth): + from members.views import dex + + metadata = {"issuer": "https://dex.example.org/", "authorization_endpoint": "https://dex.example.org/auth", "token_endpoint": "https://dex.example.org/token", "jwks_uri": "https://dex.example.org/keys"} + oauth.return_value.register.return_value.load_server_metadata.return_value = metadata + dex() + self.assertEqual(oauth.return_value.register.call_args.kwargs["server_metadata_url"], "https://dex.example.org/.well-known/openid-configuration") + metadata["issuer"] = "https://dex.example.org" + with self.assertRaisesRegex(ValueError, "issuer mismatch"): + dex() diff --git a/membership/members/views.py b/membership/members/views.py index c76e893..d93506b 100755 --- a/membership/members/views.py +++ b/membership/members/views.py @@ -140,7 +140,7 @@ def finish_registration(request): def dex(): oauth = OAuth() - remote = oauth.register("dex", client_id=settings.DEX_CLIENT_ID, client_secret=settings.DEX_CLIENT_SECRET, server_metadata_url=settings.DEX_ISSUER + "/.well-known/openid-configuration", client_kwargs={"scope": "openid email profile groups federated:id", "code_challenge_method": "S256", "timeout": 10}) + remote = oauth.register("dex", client_id=settings.DEX_CLIENT_ID, client_secret=settings.DEX_CLIENT_SECRET, server_metadata_url=settings.DEX_ISSUER.rstrip("/") + "/.well-known/openid-configuration", client_kwargs={"scope": "openid email profile groups federated:id", "code_challenge_method": "S256", "timeout": 10}) metadata = remote.load_server_metadata() if metadata.get("issuer") != settings.DEX_ISSUER: raise ValueError("Dex discovery issuer mismatch.")