From c01143945f6a2a49996c2d66202f6e0a38c92f3d Mon Sep 17 00:00:00 2001 From: erosert <47104514+EduardRosert@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:21:04 +0100 Subject: [PATCH 1/5] added claims support to custom username algo --- mozilla_django_oidc/auth.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mozilla_django_oidc/auth.py b/mozilla_django_oidc/auth.py index 5a94147a..0b50b0c1 100644 --- a/mozilla_django_oidc/auth.py +++ b/mozilla_django_oidc/auth.py @@ -3,6 +3,7 @@ import json import logging +import inspect import requests from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend @@ -20,10 +21,11 @@ LOGGER = logging.getLogger(__name__) -def default_username_algo(email): +def default_username_algo(email, claims): """Generate username for the Django user. :arg str/unicode email: the email address to use to generate a username + :arg dic claims: the claims from your OIDC provider, currently unused :returns: str/unicode @@ -100,14 +102,21 @@ def get_username(self, claims): """Generate username based on claims.""" # bluntly stolen from django-browserid # https://github.com/mozilla/django-browserid/blob/master/django_browserid/auth.py + username_algo = self.get_settings("OIDC_USERNAME_ALGO", None) if username_algo: if isinstance(username_algo, str): username_algo = import_string(username_algo) - return username_algo(claims.get("email")) + if len(inspect.getfullargspec(username_algo).args) == 1: + # this is for backwards compatibility only + return username_algo(claims.get("email")) + else: + # also pass the claims to the custom user name algo + return username_algo(claims.get("email"), claims) + - return default_username_algo(claims.get("email")) + return default_username_algo(claims.get("email"), claims) def update_user(self, user, claims): """Update existing user with new claims, if necessary save, and return user""" From 9e70a8e447d32a02ce4936c5cd37be5083d423f5 Mon Sep 17 00:00:00 2001 From: polyccon Date: Thu, 1 Jun 2023 13:33:43 +0100 Subject: [PATCH 2/5] Adds OIDC_USERNAME_ALGO to the documentation --- docs/settings.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index fbbc54f2..2b1c4e3f 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -101,6 +101,13 @@ of ``mozilla-django-oidc``. Enables or disables automatic user creation during authentication + .. py:attribute:: OIDC_USERNAME_ALGO + + :default: ``None`` + + It enables using a custom method to generate the django username from the user's + email and OIDC claims + .. py:attribute:: OIDC_STATE_SIZE :default: ``32`` From beac7c07200f26dfc00c54a4f468b085a5653f23 Mon Sep 17 00:00:00 2001 From: polyccon Date: Thu, 1 Jun 2023 13:38:23 +0100 Subject: [PATCH 3/5] Forgot the dot in the docs explanation --- docs/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.rst b/docs/settings.rst index 2b1c4e3f..8e2a9faa 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -106,7 +106,7 @@ of ``mozilla-django-oidc``. :default: ``None`` It enables using a custom method to generate the django username from the user's - email and OIDC claims + email and OIDC claims. .. py:attribute:: OIDC_STATE_SIZE From df068c055262ecc757d09f44f93fff9b078df05e Mon Sep 17 00:00:00 2001 From: erosert <47104514+EduardRosert@users.noreply.github.com> Date: Thu, 1 Jun 2023 13:40:02 +0100 Subject: [PATCH 4/5] fixed formatting for OIDC_TIMEOUT docs --- docs/settings.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 8e2a9faa..b0aa7e09 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -71,11 +71,11 @@ of ``mozilla-django-oidc``. :default: ``None`` - Defines a timeout for all requests to the OpenID Connect provider (fetch JWS, - retrieve JWT tokens, Userinfo Endpoint). The default is set to `None` which means - the library will wait indefinitely. The time can be defined as seconds (integer). - More information about possible configuration values, see Python `requests`: - https://requests.readthedocs.io/en/master/user/quickstart/#timeouts + Defines a timeout for all requests to the OpenID Connect provider (fetch JWS, + retrieve JWT tokens, Userinfo Endpoint). The default is set to `None` which means + the library will wait indefinitely. The time can be defined as seconds (integer). + More information about possible configuration values, see Python `requests`: + https://requests.readthedocs.io/en/master/user/quickstart/#timeouts .. py:attribute:: OIDC_PROXY From 35147a52b7e80852b2390cada2eb479ab0b6b310 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 1 Jun 2023 13:41:31 +0100 Subject: [PATCH 5/5] add test --- mozilla_django_oidc/auth.py | 3 +-- tests/test_auth.py | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/mozilla_django_oidc/auth.py b/mozilla_django_oidc/auth.py index 0b50b0c1..1ff2c3f7 100644 --- a/mozilla_django_oidc/auth.py +++ b/mozilla_django_oidc/auth.py @@ -21,7 +21,7 @@ LOGGER = logging.getLogger(__name__) -def default_username_algo(email, claims): +def default_username_algo(email, claims=None): """Generate username for the Django user. :arg str/unicode email: the email address to use to generate a username @@ -115,7 +115,6 @@ def get_username(self, claims): # also pass the claims to the custom user name algo return username_algo(claims.get("email"), claims) - return default_username_algo(claims.get("email"), claims) def update_user(self, user, claims): diff --git a/tests/test_auth.py b/tests/test_auth.py index 762572c1..a439dfd6 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -736,6 +736,38 @@ def test_custom_username_algo_dotted_path(self, request_mock, jws_mock): User.objects.get(username="dotted_username_algo"), ) + @override_settings( + OIDC_USE_NONCE=False, + OIDC_USERNAME_ALGO="tests.test_auth.dotted_username_algo_callback_with_claims", + ) + @patch("mozilla_django_oidc.auth.OIDCAuthenticationBackend._verify_jws") + @patch("mozilla_django_oidc.auth.requests") + def test_dotted_username_algo_callback_with_claims(self, request_mock, jws_mock): + """Test user creation with custom username algorithm with a dotted path.""" + auth_request = RequestFactory().get("/foo", {"code": "foo", "state": "bar"}) + auth_request.session = {} + + self.assertEqual(User.objects.filter(email="email@example.com").exists(), False) + jws_mock.return_value = json.dumps({"nonce": "nonce"}).encode("utf-8") + domain = "django.con" + get_json_mock = Mock() + get_json_mock.json.return_value = { + "nickname": "a_username", + "email": "email@example.com", + "domain": domain, + } + request_mock.get.return_value = get_json_mock + post_json_mock = Mock() + post_json_mock.json.return_value = { + "id_token": "id_token", + "access_token": "access_granted", + } + request_mock.post.return_value = post_json_mock + self.assertEqual( + self.backend.authenticate(request=auth_request), + User.objects.get(username=f"{domain}/email@example.com"), + ) + @override_settings(OIDC_USE_NONCE=False) @patch("mozilla_django_oidc.auth.OIDCAuthenticationBackend._verify_jws") @patch("mozilla_django_oidc.auth.requests") @@ -1170,3 +1202,9 @@ def test_returns_true_custom_claims(self, patch_logger, patch_settings): def dotted_username_algo_callback(email): return "dotted_username_algo" + + +def dotted_username_algo_callback_with_claims(email, claims=None): + domain = claims["domain"] + username = f"{domain}/{email}" + return username