diff --git a/AUTHORS b/AUTHORS index 3443635b6..52a3693af 100644 --- a/AUTHORS +++ b/AUTHORS @@ -60,6 +60,7 @@ Hasan Ramezani Hiroki Kiyohara Hossein Shakiba Islam Kamel +Ivan Lukyanets Jadiel TeĆ³filo Jens Timmerman Jerome Leclanche diff --git a/CHANGELOG.md b/CHANGELOG.md index 45414b083..d9fe0ac91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * #1337 Gracefully handle expired or deleted refresh tokens, in `validate_user`. * #1350 Support Python 3.12 and Django 5.0 * #1249 Add code_challenge_methods_supported property to auto discovery information, per [RFC 8414 section 2](https://www.rfc-editor.org/rfc/rfc8414.html#page-7) +* #1328 Adds the ability to define how to store a user profile ### Fixed diff --git a/docs/oidc.rst b/docs/oidc.rst index bbb4651bd..ac9c97161 100644 --- a/docs/oidc.rst +++ b/docs/oidc.rst @@ -404,6 +404,17 @@ In the docs below, it assumes that you have mounted the the URLs accordingly. +Define where to store the profile +================================= + +.. py:function:: OAuth2Validator.get_or_create_user_from_content(content) + +An optional layer to define where to store the profile in ``UserModel`` or a separate model. For example ``UserOAuth``, where ``user = models.OneToOneField(UserModel)``. + +The function is called after checking that the username is present in the content. + +:return: An instance of the ``UserModel`` representing the user fetched or created. + ConnectDiscoveryInfoView ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index cecb843c5..829cde25f 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -333,6 +333,17 @@ def validate_client_id(self, client_id, request, *args, **kwargs): def get_default_redirect_uri(self, client_id, request, *args, **kwargs): return request.client.default_redirect_uri + def get_or_create_user_from_content(self, content): + """ + An optional layer to define where to store the profile in `UserModel` or a separate model. For example `UserOAuth`, where `user = models.OneToOneField(UserModel)` . + + The function is called after checking that username is in the content. + + Returns an UserModel instance; + """ + user, _ = UserModel.objects.get_or_create(**{UserModel.USERNAME_FIELD: content["username"]}) + return user + def _get_token_from_authentication_server( self, token, introspection_url, introspection_token, introspection_credentials ): @@ -383,9 +394,7 @@ def _get_token_from_authentication_server( if "active" in content and content["active"] is True: if "username" in content: - user, _created = UserModel.objects.get_or_create( - **{UserModel.USERNAME_FIELD: content["username"]} - ) + user = self.get_or_create_user_from_content(content) else: user = None diff --git a/tests/test_oauth2_validators.py b/tests/test_oauth2_validators.py index cb734a9b2..ca80aedb0 100644 --- a/tests/test_oauth2_validators.py +++ b/tests/test_oauth2_validators.py @@ -335,6 +335,14 @@ def test_save_bearer_token__with_new_token__calls_methods_to_create_access_and_r assert create_access_token_mock.call_count == 1 assert create_refresh_token_mock.call_count == 1 + def test_get_or_create_user_from_content(self): + content = {"username": "test_user"} + UserModel.objects.filter(username=content["username"]).delete() + user = self.validator.get_or_create_user_from_content(content) + + self.assertIsNotNone(user) + self.assertEqual(content["username"], user.username) + class TestOAuth2ValidatorProvidesErrorData(TransactionTestCase): """These test cases check that the recommended error codes are returned