Repository: jazzband/django-oauth-toolkit

Issue:
Backwards-incompatible API change in OAuth2Validator in 1.6.0
**Describe the bug**

PR #967 changed the API for `OAuth2Validator.get_additional_claims` in a backwards-incompatible way (and the project major version has not been bumped, which would have been required by semantic versioning).

Specifically, the `request` argument was removed, which we relied upon downstream. It seems that the functionality has remained (in a slightly more complex way), but such breakage is not expected between minor versions.

I propose to revert the change in 1.6.1, and make a new release 2.0.0 introducing the breaking change.

**Expected behavior**

We expect the (documented) API not to break backwords-incompatibly in minor versions.

**Version**
1.6.0


Discussion:
Then again, reverting the change in a 1.6.1 release would be a breaking change compared to 1.6.0. So the situation cannot be fixed, unless a 1.6.1 release re-introduces the old API, but keeps the new API as well.

But I am afraid this is what needs to be done, because downstream, we cannot really fix this without breaking the API for our modules even further downstream, and pinning the dependency on `django-oauth-toolkit` to `<1.6` will probably have us missing out on security fixes.

EDIT: OK, we actually can. For anyone interested in what a workaround for such a breaking change in a complex downstream app might look like, see here: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/blob/5cae28a03b7bf9dec9390fcb53024600792f6c99/aleksis/core/util/auth_helpers.py#L47
Oh, that's not good. 

I'd say we should put fixing this on the queue for 1.7.0 but then you'd say it's a major change and it should be 2.0. We've tried to adopt semver -- the project previously had not used it -- and it seems we missed this as being a breaking fix.

Need to research what happened here and come up with a plan. Meanwhile go ahead and temporarily pin <1.6.0.


Resolving patch:
diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py
--- a/oauth2_provider/oauth2_validators.py
+++ b/oauth2_provider/oauth2_validators.py
@@ -740,24 +740,15 @@ def _save_id_token(self, jti, request, expires, *args, **kwargs):
     def get_jwt_bearer_token(self, token, token_handler, request):
         return self.get_id_token(token, token_handler, request)
 
-    def get_claim_list(self):
-        def get_sub_code(request):
-            return str(request.user.id)
-
-        list = [("sub", get_sub_code)]
+    def get_oidc_claims(self, token, token_handler, request):
+        # Required OIDC claims
+        claims = {
+            "sub": str(request.user.id),
+        }
 
         # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
-        add = self.get_additional_claims()
-        list.extend(add)
-
-        return list
+        claims.update(**self.get_additional_claims(request))
 
-    def get_oidc_claims(self, token, token_handler, request):
-        data = self.get_claim_list()
-        claims = {}
-
-        for k, call in data:
-            claims[k] = call(request)
         return claims
 
     def get_id_token_dictionary(self, token, token_handler, request):
@@ -910,5 +901,5 @@ def get_userinfo_claims(self, request):
         """
         return self.get_oidc_claims(None, None, request)
 
-    def get_additional_claims(self):
-        return []
+    def get_additional_claims(self, request):
+        return {}
diff --git a/oauth2_provider/views/oidc.py b/oauth2_provider/views/oidc.py
--- a/oauth2_provider/views/oidc.py
+++ b/oauth2_provider/views/oidc.py
@@ -45,13 +45,6 @@ def get(self, request, *args, **kwargs):
         signing_algorithms = [Application.HS256_ALGORITHM]
         if oauth2_settings.OIDC_RSA_PRIVATE_KEY:
             signing_algorithms = [Application.RS256_ALGORITHM, Application.HS256_ALGORITHM]
-
-        validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
-        validator = validator_class()
-        oidc_claims = []
-        for el, _ in validator.get_claim_list():
-            oidc_claims.append(el)
-
         data = {
             "issuer": issuer_url,
             "authorization_endpoint": authorization_endpoint,
@@ -64,7 +57,6 @@ def get(self, request, *args, **kwargs):
             "token_endpoint_auth_methods_supported": (
                 oauth2_settings.OIDC_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED
             ),
-            "claims_supported": oidc_claims,
         }
         response = JsonResponse(data)
         response["Access-Control-Allow-Origin"] = "*"


Test patch:
diff --git a/tests/test_oidc_views.py b/tests/test_oidc_views.py
--- a/tests/test_oidc_views.py
+++ b/tests/test_oidc_views.py
@@ -29,7 +29,6 @@ def test_get_connect_discovery_info(self):
             "subject_types_supported": ["public"],
             "id_token_signing_alg_values_supported": ["RS256", "HS256"],
             "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
-            "claims_supported": ["sub"],
         }
         response = self.client.get(reverse("oauth2_provider:oidc-connect-discovery-info"))
         self.assertEqual(response.status_code, 200)
@@ -56,7 +55,6 @@ def test_get_connect_discovery_info_without_issuer_url(self):
             "subject_types_supported": ["public"],
             "id_token_signing_alg_values_supported": ["RS256", "HS256"],
             "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
-            "claims_supported": ["sub"],
         }
         response = self.client.get(reverse("oauth2_provider:oidc-connect-discovery-info"))
         self.assertEqual(response.status_code, 200)
@@ -148,21 +146,11 @@ def test_userinfo_endpoint_bad_token(oidc_tokens, client):
     assert rsp.status_code == 401
 
 
-EXAMPLE_EMAIL = "example.email@example.com"
-
-
-def claim_user_email(request):
-    return EXAMPLE_EMAIL
-
-
 @pytest.mark.django_db
 def test_userinfo_endpoint_custom_claims(oidc_tokens, client, oauth2_settings):
     class CustomValidator(OAuth2Validator):
-        def get_additional_claims(self):
-            return [
-                ("username", claim_user_email),
-                ("email", claim_user_email),
-            ]
+        def get_additional_claims(self, request):
+            return {"state": "very nice"}
 
     oidc_tokens.oauth2_settings.OAUTH2_VALIDATOR_CLASS = CustomValidator
     auth_header = "Bearer %s" % oidc_tokens.access_token
@@ -173,9 +161,5 @@ def get_additional_claims(self):
     data = rsp.json()
     assert "sub" in data
     assert data["sub"] == str(oidc_tokens.user.pk)
-
-    assert "username" in data
-    assert data["username"] == EXAMPLE_EMAIL
-
-    assert "email" in data
-    assert data["email"] == EXAMPLE_EMAIL
+    assert "state" in data
+    assert data["state"] == "very nice"