Repository: pennersr/django-allauth
Pull request #3832 (2024-05-21 00:36:02)

fix(kakao): Change deprecated properties to profile information
# Submitting Pull Requests

## General

 - [x] Make sure you use [semantic commit messages](https://seesparkbox.com/foundry/semantic_commit_messages).
       Examples: `"fix(google): Fixed foobar bug"`, `"feat(accounts): Added foobar feature"`.
 - [x] All Python code must formatted using Black, and clean from pep8 and isort issues.
 - [ ] JavaScript code should adhere to [StandardJS](https://standardjs.com).
 - [ ] If your changes are significant, please update `ChangeLog.rst`.
 - [ ] If your change is substantial, feel free to add yourself to `AUTHORS`.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/67603687/badge)](https://coveralls.io/builds/67603687)

coverage: 95.718%. remained the same
when pulling **bddd0022a470c35b07364d900be8ecb87898083a on juchajam:main**
into **1c8b41f6eefdd25912d1819408403d0c885a7518 on pennersr:main**.


Patch:
diff --git a/ChangeLog.rst b/ChangeLog.rst
index 3b130f42..cd82b8de 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -10,6 +10,12 @@
 - In case a headless attempt was made to connect a third-party account that was already
   connected to a different account, no error was communicated to the frontend. Fixed.
 
+- When the headless provider signup endpoint was called while that flow was not pending,
+  a crash would occur. This has been fixed to return a 409 (conflict).
+
+- Microsoft provider: the URLs pointing to the login and graph API are now
+  configurable via the app settings.
+
 
 0.63.1 (2024-05-17)
 *******************
diff --git a/allauth/headless/base/response.py b/allauth/headless/base/response.py
index 96216e45..3ff235b8 100644
--- a/allauth/headless/base/response.py
+++ b/allauth/headless/base/response.py
@@ -99,6 +99,11 @@ class ForbiddenResponse(APIResponse):
         super().__init__(request, status=403)
 
 
+class ConflictResponse(APIResponse):
+    def __init__(self, request):
+        super().__init__(request, status=409)
+
+
 def user_data(user):
     """Basic user data, also exposed in partly authenticated scenario's
     (e.g. password reset, email verification).
diff --git a/allauth/headless/socialaccount/views.py b/allauth/headless/socialaccount/views.py
index 485ecc47..8c254448 100644
--- a/allauth/headless/socialaccount/views.py
+++ b/allauth/headless/socialaccount/views.py
@@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
 from allauth.core.exceptions import SignupClosedException
 from allauth.headless.base.response import (
     AuthenticationResponse,
+    ConflictResponse,
     ForbiddenResponse,
 )
 from allauth.headless.base.views import APIView, AuthenticatedAPIView
@@ -26,16 +27,21 @@ from allauth.socialaccount.models import SocialAccount
 class ProviderSignupView(APIView):
     input_class = SignupInput
 
-    def post(self, request, *args, **kwargs):
+    def handle(self, request, *args, **kwargs):
+        self.sociallogin = flows.signup.get_pending_signup(self.request)
+        if not self.sociallogin:
+            return ConflictResponse(request)
         if not get_socialaccount_adapter().is_open_for_signup(
             request, self.sociallogin
         ):
             return ForbiddenResponse(request)
+        return super().handle(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
         flows.signup.signup_by_form(self.request, self.sociallogin, self.input)
         return AuthenticationResponse(request)
 
     def get_input_kwargs(self):
-        self.sociallogin = flows.signup.get_pending_signup(self.request)
         return {"sociallogin": self.sociallogin}
 
 
diff --git a/allauth/socialaccount/providers/kakao/provider.py b/allauth/socialaccount/providers/kakao/provider.py
index 026a4d35..2b8ae970 100644
--- a/allauth/socialaccount/providers/kakao/provider.py
+++ b/allauth/socialaccount/providers/kakao/provider.py
@@ -7,14 +7,20 @@ from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
 class KakaoAccount(ProviderAccount):
     @property
     def properties(self):
-        return self.account.extra_data.get("properties")
+        return self.account.extra_data.get("properties", {})
+
+    @property
+    def profile(self):
+        return self.account.extra_data.get("kakao_account", {}).get("profile", {})
 
     def get_avatar_url(self):
-        return self.properties.get("profile_image")
+        return self.profile.get(
+            "profile_image_url", self.properties.get("profile_image")
+        )
 
     def to_str(self):
         dflt = super(KakaoAccount, self).to_str()
-        return self.properties.get("nickname", dflt)
+        return self.profile.get("nickname", self.properties.get("nickname", dflt))
 
 
 class KakaoProvider(OAuth2Provider):
@@ -28,7 +34,7 @@ class KakaoProvider(OAuth2Provider):
 
     def extract_common_fields(self, data):
         email = data.get("kakao_account", {}).get("email")
-        nickname = data.get("properties", {}).get("nickname")
+        nickname = data.get("kakao_account", {}).get("profile", {}).get("nickname")
 
         return dict(email=email, username=nickname)
 
diff --git a/allauth/socialaccount/providers/microsoft/views.py b/allauth/socialaccount/providers/microsoft/views.py
index bfe354f8..d1751a00 100644
--- a/allauth/socialaccount/providers/microsoft/views.py
+++ b/allauth/socialaccount/providers/microsoft/views.py
@@ -39,7 +39,8 @@ class MicrosoftGraphOAuth2Adapter(OAuth2Adapter):
         # Prefer app based tenant setting.
         app = get_adapter().get_app(context.request, provider=self.provider_id)
         tenant = app.settings.get("tenant", tenant)
-        return f"https://login.microsoftonline.com/{tenant}{path}"
+        login_url = app.settings.get("login_url", "https://login.microsoftonline.com")
+        return f"{login_url}/{tenant}{path}"
 
     @property
     def access_token_url(self):
@@ -49,7 +50,11 @@ class MicrosoftGraphOAuth2Adapter(OAuth2Adapter):
     def authorize_url(self):
         return self._build_tenant_url("/oauth2/v2.0/authorize")
 
-    profile_url = "https://graph.microsoft.com/v1.0/me"
+    @property
+    def profile_url(self):
+        app = get_adapter().get_app(context.request, provider=self.provider_id)
+        graph_url = app.settings.get("graph_url", "https://graph.microsoft.com")
+        return f"{graph_url}/v1.0/me"
 
     user_properties = (
         "businessPhones",
diff --git a/docs/headless/openapi-specification/openapi.yaml b/docs/headless/openapi-specification/openapi.yaml
index 4a16f670..d8c669f2 100644
--- a/docs/headless/openapi-specification/openapi.yaml
+++ b/docs/headless/openapi-specification/openapi.yaml
@@ -425,6 +425,13 @@ paths:
             application/json:
               schema:
                 $ref: "#/components/schemas/ForbiddenResponse"
+        "409":
+          description: |
+            Conflict. The provider signup flow is not pending.
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/ConflictResponse"
   ######################################################################
   # Authentication: 2FA
   ######################################################################
@@ -1426,6 +1433,15 @@ components:
             - 403
       required:
         - status
+    ConflictResponse:
+      type: object
+      properties:
+        status:
+          type: integer
+          enum:
+            - 409
+      required:
+        - status
     ReauthenticationResponse:
       type: object
       description: |
diff --git a/docs/socialaccount/providers/microsoft.rst b/docs/socialaccount/providers/microsoft.rst
index f6aacde9..43a8dbd0 100644
--- a/docs/socialaccount/providers/microsoft.rst
+++ b/docs/socialaccount/providers/microsoft.rst
@@ -20,6 +20,9 @@ for the login. To restrict it, change the ``tenant`` setting as shown below.
                   "secret": "<insert-secret>",
                   "settings": {
                       "tenant": "organizations",
+                      # Optional: override URLs (use base URLs without path)
+                      "login_url": "https://login.microsoftonline.com",
+                      "graph_url": "https://graph.microsoft.com",
                   }
               }
           ]
diff --git a/docs/socialaccount/signals.rst b/docs/socialaccount/signals.rst
index 01457d28..210758d3 100644
--- a/docs/socialaccount/signals.rst
+++ b/docs/socialaccount/signals.rst
@@ -13,7 +13,9 @@ hook to them for your own needs.
     provided.
 
 - ``allauth.socialaccount.signals.social_account_added(request, sociallogin)``
-    Sent after a user connects a social account to their local account.
+    Sent after a user connects a social account to their local account. This
+    is an explicit action and does not get called for the creation of a
+    socialaccount.
 
 - ``allauth.socialaccount.signals.social_account_updated(request, sociallogin)``
     Sent after a social account has been updated. This happens when a user


Test patch:
diff --git a/allauth/headless/socialaccount/tests/test_views.py b/allauth/headless/socialaccount/tests/test_views.py
index 974daaeb..4e32c534 100644
--- a/allauth/headless/socialaccount/tests/test_views.py
+++ b/allauth/headless/socialaccount/tests/test_views.py
@@ -377,3 +377,14 @@ def test_token_connect_already_connected(
             }
         ],
     }
+
+
+def test_provider_signup_not_pending(client, headless_reverse, db, settings):
+    resp = client.post(
+        headless_reverse("headless:socialaccount:provider_signup"),
+        data={
+            "email": "a@b.com",
+        },
+        content_type="application/json",
+    )
+    assert resp.status_code == 409
diff --git a/allauth/socialaccount/providers/kakao/tests.py b/allauth/socialaccount/providers/kakao/tests.py
index 999441cc..745f6964 100644
--- a/allauth/socialaccount/providers/kakao/tests.py
+++ b/allauth/socialaccount/providers/kakao/tests.py
@@ -10,22 +10,43 @@ class KakaoTests(OAuth2TestsMixin, TestCase):
     kakao_data = """
         {
             "id": 123456789,
-            "properties": {
-                "nickname": "\uc9c0\uc724",
-                "thumbnail_image": "http://xxx.kakao.co.kr/.../aaa.jpg",
-                "profile_image": "http://xxx.kakao.co.kr/.../bbb.jpg"
-            },
+            "connected_at": "2022-04-11T01:45:28Z",
             "kakao_account": {
-                "has_email": true,
+                "profile_nickname_needs_agreement": false,
+                "profile_image_needs_agreement": false,
+                "profile": {
+                    "nickname": "홍길동",
+                    "thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
+                    "profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
+                    "is_default_image":false,
+                    "is_default_nickname": false
+                },
+                "name_needs_agreement":false,
+                "name":"홍길동",
+                "email_needs_agreement":false,
                 "is_email_valid": true,
                 "is_email_verified": true,
-                "email": "xxxxxxx@xxxxx.com",
-                "has_age_range": true,
-                "age_range": "20~29",
-                "has_birthday": true,
-                "birthday": "1130",
-                "has_gender": true,
-                "gender": "female"
+                "email": "sample@sample.com",
+                "age_range_needs_agreement":false,
+                "age_range":"20~29",
+                "birthyear_needs_agreement": false,
+                "birthyear": "2002",
+                "birthday_needs_agreement":false,
+                "birthday":"1130",
+                "birthday_type":"SOLAR",
+                "gender_needs_agreement":false,
+                "gender":"female",
+                "phone_number_needs_agreement": false,
+                "phone_number": "+82 010-1234-5678",
+                "ci_needs_agreement": false,
+                "ci": "CI",
+                "ci_authenticated_at": "2019-03-11T11:25:22Z"
+            },
+            "properties":{
+                "CUSTOM_PROPERTY_KEY": "CUSTOM_PROPERTY_VALUE"
+            },
+            "for_partner": {
+                "uuid": "UUID"
             }
         }
     """