Repository: python-social-auth/social-core
Pull request #1147 (2025-05-06 20:22:55)

Revert LinkedIn OAuth2
The previous PR #950 mistakenly replaced the OAuth2 endpoint with the OpenID Connect one. Users who use OpenID Connect should use `LinkedinOpenIdConnect`. For existing LinkedIn OAuth2 users like myself, `LinkedinOAuth2` should still work. 

Just to clarify: LinkedIn has **NOT** removed the `r_liteprofile` scope for OAuth2 users — it's still functioning properly. If anyone is having issues with scopes, please check the “Products” tab in your LinkedIn App Setup page.

For example, here we have both OAuth2 and OpenID Connect enabled:
![image](https://github.com/user-attachments/assets/8b05e29f-4dea-4b86-82d9-900166f04b51)
![image](https://github.com/user-attachments/assets/517b0f34-f24c-458b-93b2-037a55d92d81)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) Report
Attention: Patch coverage is `78.57143%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 77.90%. Comparing base [(`345ad5f`)](https://app.codecov.io/gh/python-social-auth/social-core/commit/345ad5ffd786ae11c430dfee2d8b1f58df37be49?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) to head [(`aade9d7`)](https://app.codecov.io/gh/python-social-auth/social-core/commit/aade9d7e42e2476eebe96f0e709be52e9b761ea0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).

| [Files with missing lines](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | Patch % | Lines |
|---|---|---|
| [social\_core/backends/linkedin.py](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147?src=pr&el=tree&filepath=social_core%2Fbackends%2Flinkedin.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth#diff-c29jaWFsX2NvcmUvYmFja2VuZHMvbGlua2VkaW4ucHk=) | 76.92% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1147      +/-   ##
==========================================
+ Coverage   77.87%   77.90%   +0.03%     
==========================================
  Files         353      353              
  Lines       10794    10796       +2     
  Branches      471      470       -1     
==========================================
+ Hits         8406     8411       +5     
+ Misses       2222     2219       -3     
  Partials      166      166              
```

| [Flag](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | `77.90% <78.57%> (+0.03%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/python-social-auth/social-core/pull/1147?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py
index dcc49bb8..d3c6ac76 100644
--- a/social_core/backends/linkedin.py
+++ b/social_core/backends/linkedin.py
@@ -48,13 +48,13 @@ class LinkedinOAuth2(BaseOAuth2):
     name = "linkedin-oauth2"
     AUTHORIZATION_URL = "https://www.linkedin.com/oauth/v2/authorization"
     ACCESS_TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"
-    USER_DETAILS_URL = "https://api.linkedin.com/v2/userinfo?projection=({projection})"
+    USER_DETAILS_URL = "https://api.linkedin.com/v2/me?projection=({projection})"
     USER_EMAILS_URL = (
         "https://api.linkedin.com/v2/emailAddress"
         "?q=members&projection=(elements*(handle~))"
     )
     REDIRECT_STATE = False
-    DEFAULT_SCOPE = ["email", "profile", "openid"]
+    DEFAULT_SCOPE = ["r_liteprofile"]
     EXTRA_DATA = [
         ("id", "id"),
         ("expires_in", "expires"),
@@ -65,7 +65,14 @@ class LinkedinOAuth2(BaseOAuth2):
     ]
 
     def user_details_url(self):
-        return self.USER_DETAILS_URL
+        # use set() since LinkedIn fails when values are duplicated
+        fields_selectors = list(
+            {"id", "firstName", "lastName", *self.setting("FIELD_SELECTORS", [])}
+        )
+        # user sort to ease the tests URL mocking
+        fields_selectors.sort()
+        fields_selectors = ",".join(fields_selectors)
+        return self.USER_DETAILS_URL.format(projection=fields_selectors)
 
     def user_emails_url(self):
         return self.USER_EMAILS_URL
@@ -75,10 +82,10 @@ class LinkedinOAuth2(BaseOAuth2):
             self.user_details_url(), headers=self.user_data_headers(access_token)
         )
 
-        if "email" in set(self.setting("FIELD_SELECTORS", [])):
+        if "emailAddress" in set(self.setting("FIELD_SELECTORS", [])):
             emails = self.email_data(access_token, *args, **kwargs)
             if emails:
-                response["email"] = emails[0]
+                response["emailAddress"] = emails[0]
 
         return response
 
@@ -88,20 +95,38 @@ class LinkedinOAuth2(BaseOAuth2):
         )
         email_addresses = []
         for element in response.get("elements", []):
-            email_address = element.get("handle~", {}).get("email")
+            email_address = element.get("handle~", {}).get("emailAddress")
             email_addresses.append(email_address)
         return list(filter(None, email_addresses))
 
     def get_user_details(self, response):
         """Return user details from Linkedin account"""
-        response = self.user_data(access_token=response["access_token"])
+
+        def get_localized_name(name):
+            """
+            FirstName & Last Name object
+            {
+                  'localized': {
+                     'en_US': 'Smith'
+                  },
+                  'preferredLocale': {
+                     'country': 'US',
+                     'language': 'en'
+                  }
+            }
+            :return the localizedName from the lastName object
+            """
+            locale = "{}_{}".format(
+                name["preferredLocale"]["language"], name["preferredLocale"]["country"]
+            )
+            return name["localized"].get(locale, "")
+
         fullname, first_name, last_name = self.get_user_names(
-            first_name=response["given_name"],
-            last_name=response["family_name"],
+            first_name=get_localized_name(response["firstName"]),
+            last_name=get_localized_name(response["lastName"]),
         )
-        email = response.get("email", "")
+        email = response.get("emailAddress", "")
         return {
-            "id": response.get("sub", ""),
             "username": first_name + last_name,
             "fullname": fullname,
             "first_name": first_name,


Test patch:
diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py
index 6a90d489..0f3bcff0 100644
--- a/social_core/tests/backends/test_linkedin.py
+++ b/social_core/tests/backends/test_linkedin.py
@@ -41,23 +41,24 @@ class LinkedinOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin):
 
 
 class BaseLinkedinTest:
-    user_data_url = "https://api.linkedin.com/v2/userinfo"
+    user_data_url = "https://api.linkedin.com/v2/me?projection=(firstName,id,lastName)"
     expected_username = "FooBar"
     access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"})
 
     # Reference:
-    # https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self
-    # -serve/sign-in-with-linkedin-v2#response-body-schema
+    # https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self
+    # -serve/sign-in-with-linkedin?context=linkedin/consumer/context#api-request
     user_data_body = json.dumps(
         {
-            "sub": "782bbtaQ",
-            "name": "FooBar",
-            "given_name": "Foo",
-            "family_name": "Bar",
-            "picture": "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/",
-            "locale": "en-US",
-            "email": "doe@email.com",
-            "email_verified": True,
+            "id": "1010101010",
+            "firstName": {
+                "localized": {"en_US": "Foo"},
+                "preferredLocale": {"country": "US", "language": "en"},
+            },
+            "lastName": {
+                "localized": {"en_US": "Bar"},
+                "preferredLocale": {"country": "US", "language": "en"},
+            },
         }
     )