Skip to content

Commit

Permalink
refactor(linkedin_oauth2): Bump to API v2
Browse files Browse the repository at this point in the history
  • Loading branch information
pennersr authored and Raymond Penners committed Feb 26, 2019
1 parent 1271e67 commit 5f90cc3
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 40 deletions.
7 changes: 7 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ Note worthy changes

- Add testing for Django 2.2 (no code changes required)

Backwards incompatible changes
------------------------------

- ``linkedin_oauth2``: As the LinkedIn V1 API is deprecated, the user info
endpoint has been moved over to use the API V2. The format of the user
``extra_data`` is different and profile picture is absent by default.


0.38.0 (2018-10-03)
*******************
Expand Down
80 changes: 49 additions & 31 deletions allauth/socialaccount/providers/linkedin_oauth2/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,43 @@
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider


class LinkedInOAuth2Account(ProviderAccount):
def get_profile_url(self):
return self.account.extra_data.get('publicProfileUrl')
def _extract_name_field(data, field_name):
ret = ''
v = data.get(field_name, {})
if v:
localized = v.get('localized', {})
preferred_locale = v.get(
'preferredLocale', {'country': 'US', 'language': 'en'})
locale_key = '_'.join([
preferred_locale['language'],
preferred_locale['country']])
if locale_key in localized:
ret = localized.get(locale_key)
elif localized:
ret = next(iter(localized.values()))
return ret


def get_avatar_url(self):
# try to return the higher res picture-urls::(original) first
try:
return self.account.extra_data['pictureUrls']['values'][0]
except Exception:
# if we can't get higher res for any reason, we'll just return the
# low res
pass
return self.account.extra_data.get('pictureUrl')
def _extract_email(data):
"""
{'elements': [{'handle': 'urn:li:emailAddress:319371470',
'handle~': {'emailAddress': 'raymond.penners@intenct.nl'}}]}
"""
ret = ''
elements = data.get('elements', [])
if len(elements) > 0:
ret = elements[0].get('handle~', {}).get('emailAddress', '')
return ret


class LinkedInOAuth2Account(ProviderAccount):
def to_str(self):
dflt = super(LinkedInOAuth2Account, self).to_str()
name = self.account.extra_data.get('name', dflt)
first_name = self.account.extra_data.get('firstName', None)
last_name = self.account.extra_data.get('lastName', None)
if first_name and last_name:
name = first_name + ' ' + last_name
return name
ret = super(LinkedInOAuth2Account, self).to_str()
first_name = _extract_name_field(self.account.extra_data, 'firstName')
last_name = _extract_name_field(self.account.extra_data, 'lastName')
if first_name or last_name:
ret = ' '.join([first_name, last_name]).strip()
return ret


class LinkedInOAuth2Provider(OAuth2Provider):
Expand All @@ -45,28 +60,31 @@ def extract_uid(self, data):
return str(data['id'])

def get_profile_fields(self):
default_fields = ['id',
'first-name',
'last-name',
'email-address',
'picture-url',
# picture-urls::(original) is higher res
'picture-urls::(original)',
'public-profile-url']
default_fields = [
'id',
'firstName',
'lastName',
# This would be needed to in case you need access to the image
# URL. Not enabling this by default due to the amount of data
# returned.
#
# 'profilePicture(displayImage~:playableStreams)'
]
fields = self.get_settings().get('PROFILE_FIELDS',
default_fields)
return fields

def get_default_scope(self):
scope = ['r_basicprofile']
scope = ['r_liteprofile']
if app_settings.QUERY_EMAIL:
scope.append('r_emailaddress')
return scope

def extract_common_fields(self, data):
return dict(email=data.get('emailAddress'),
first_name=data.get('firstName'),
last_name=data.get('lastName'))
return dict(
first_name=_extract_name_field(data, 'firstName'),
last_name=_extract_name_field(data, 'lastName'),
email=_extract_email(data))


provider_classes = [LinkedInOAuth2Provider]
28 changes: 22 additions & 6 deletions allauth/socialaccount/providers/linkedin_oauth2/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,27 @@ class LinkedInOAuth2Tests(OAuth2TestsMixin, TestCase):
def get_mocked_response(self):
return MockedResponse(200, """
{
"emailAddress": "raymond.penners@intenct.nl",
"firstName": "Raymond",
"id": "ZLARGMFT1M",
"lastName": "Penners",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_e0hbvSLc",
"publicProfileUrl": "http://www.linkedin.com/in/intenct"
"profilePicture": {
"displayImage": "urn:li:digitalmediaAsset:12345abcdefgh-12abcd"
},
"id": "1234567",
"lastName": {
"preferredLocale": {
"language": "en",
"country": "US"
},
"localized": {
"en_US": "Penners"
}
},
"firstName": {
"preferredLocale": {
"language": "en",
"country": "US"
},
"localized": {
"en_US": "Raymond"
}
}
}
""")
19 changes: 16 additions & 3 deletions allauth/socialaccount/providers/linkedin_oauth2/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import requests

from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
Expand All @@ -13,7 +14,8 @@ class LinkedInOAuth2Adapter(OAuth2Adapter):
provider_id = LinkedInOAuth2Provider.id
access_token_url = 'https://www.linkedin.com/oauth/v2/accessToken'
authorize_url = 'https://www.linkedin.com/oauth/v2/authorization'
profile_url = 'https://api.linkedin.com/v1/people/~'
profile_url = 'https://api.linkedin.com/v2/me'
email_url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))' # noqa
# See:
# http://developer.linkedin.com/forum/unauthorized-invalid-or-expired-token-immediately-after-receiving-oauth2-token?page=1 # noqa
access_token_method = 'GET'
Expand All @@ -25,13 +27,24 @@ def complete_login(self, request, app, token, **kwargs):

def get_user_info(self, token):
fields = self.get_provider().get_profile_fields()
url = self.profile_url + ':(%s)?format=json' % ','.join(fields)

headers = {}
headers.update(self.get_provider().get_settings().get('HEADERS', {}))
headers['Authorization'] = ' '.join(['Bearer', token.token])

info = {}
if app_settings.QUERY_EMAIL:
resp = requests.get(self.email_url, headers=headers)
# If this response goes wrong, that is not a blocker in order to
# continue.
if resp.ok:
info = resp.json()

url = self.profile_url + '?projection=(%s)' % ','.join(fields)
resp = requests.get(url, headers=headers)
resp.raise_for_status()
return resp.json()
info.update(resp.json())
return info


oauth2_login = OAuth2LoginView.adapter_view(LinkedInOAuth2Adapter)
Expand Down

0 comments on commit 5f90cc3

Please sign in to comment.