diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 9eb6c2c8..00000000 --- a/.github/stale.yml +++ /dev/null @@ -1,12 +0,0 @@ -daysUntilStale: 60 -daysUntilClose: 7 -exemptLabels: - - pinned - - security -staleLabel: stale -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -closeComment: false -only: issues diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..8e13fd73 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-merge-conflict + - id: check-json + - id: debug-statements + - id: mixed-line-ending + args: [--fix=lf] + - repo: https://github.com/asottile/pyupgrade + rev: v2.29.1 + hooks: + - id: pyupgrade + args: [--py36-plus] + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd4f866..e2182cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Add fields that populate on create but not update `SOCIAL_AUTH_IMMUTABLE_USER_FIELDS` - Add Gitea oauth2 backend +- Add Twitch OpenId backend ### Changed - Fixed Slack user identity API call with Bearer headers - Fixed microsoft-graph login error +- Fixed Twitch OAuth2 backend ## [4.1.0](https://github.com/python-social-auth/social-core/releases/tag/4.1.0) - 2021-03-01 diff --git a/setup.py b/setup.py index e94f7674..897f4214 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import re from os.path import join, dirname @@ -21,7 +20,7 @@ def long_description(): try: return open(join(dirname(__file__), 'README.md')).read() - except IOError: + except OSError: return None @@ -33,12 +32,12 @@ def read_version(): def read_requirements(filename): - with open(filename, 'r') as file: + with open(filename) as file: return [line for line in file.readlines() if not line.startswith('-')] def read_tests_requirements(filename): - return read_requirements('social_core/tests/{0}'.format(filename)) + return read_requirements(f'social_core/tests/{filename}') requirements = read_requirements('requirements-base.txt') diff --git a/social_core/actions.py b/social_core/actions.py index eee563d8..81427156 100644 --- a/social_core/actions.py +++ b/social_core/actions.py @@ -92,7 +92,7 @@ def do_complete(backend, login, user=None, redirect_name='next', if redirect_value and redirect_value != url: redirect_value = quote(redirect_value) url += ('&' if '?' in url else '?') + \ - '{0}={1}'.format(redirect_name, redirect_value) + f'{redirect_name}={redirect_value}' if backend.setting('SANITIZE_REDIRECTS', True): allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \ diff --git a/social_core/backends/apple.py b/social_core/backends/apple.py index edfab315..42d707ec 100644 --- a/social_core/backends/apple.py +++ b/social_core/backends/apple.py @@ -128,8 +128,8 @@ def decode_id_token(self, id_token): audience=self.get_audience(), algorithms=['RS256'], ) - except PyJWTError: - raise AuthFailed(self, 'Token validation failed') + except PyJWTError as error: + raise AuthFailed(self, f'Token validation failed by {error}') return decoded diff --git a/social_core/backends/asana.py b/social_core/backends/asana.py index d19fd140..7e050378 100644 --- a/social_core/backends/asana.py +++ b/social_core/backends/asana.py @@ -28,7 +28,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): return self.get_json(self.USER_DATA_URL, headers={ - 'Authorization': 'Bearer {}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) def extra_data(self, user, uid, response, details=None, *args, **kwargs): diff --git a/social_core/backends/atlassian.py b/social_core/backends/atlassian.py index f76969f9..cb8e09a7 100644 --- a/social_core/backends/atlassian.py +++ b/social_core/backends/atlassian.py @@ -30,8 +30,8 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): resources = self.get_json('https://api.atlassian.com/oauth/token/accessible-resources', - headers={'Authorization': 'Bearer {}'.format(access_token)}) + headers={'Authorization': f'Bearer {access_token}'}) user_info = self.get_json('https://api.atlassian.com/ex/jira/{}/rest/api/2/myself'.format(resources[0]['id']), - headers={'Authorization': 'Bearer {}'.format(access_token)}) + headers={'Authorization': f'Bearer {access_token}'}) user_info['resources'] = resources return user_info diff --git a/social_core/backends/azuread.py b/social_core/backends/azuread.py index 3ca9908e..2e37af51 100644 --- a/social_core/backends/azuread.py +++ b/social_core/backends/azuread.py @@ -45,7 +45,7 @@ class AzureADOAuth2(BaseOAuth2): ACCESS_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/token' ACCESS_TOKEN_METHOD = 'POST' REDIRECT_STATE = False - DEFAULT_SCOPE = ['openid', 'profile', 'user_impersonation'] + DEFAULT_SCOPE = ['openid', 'profile', 'user_impersonation', 'email'] EXTRA_DATA = [ ('access_token', 'access_token'), ('id_token', 'id_token'), @@ -70,7 +70,7 @@ def get_user_details(self, response): response.get('family_name', '') ) return {'username': fullname, - 'email': response.get('upn'), + 'email': response.get('email', response.get('upn')), 'fullname': fullname, 'first_name': first_name, 'last_name': last_name} diff --git a/social_core/backends/azuread_b2c.py b/social_core/backends/azuread_b2c.py index 7662f41e..a9f3842c 100644 --- a/social_core/backends/azuread_b2c.py +++ b/social_core/backends/azuread_b2c.py @@ -165,7 +165,7 @@ def get_public_key(self, kid): for key in resp.json()['keys']: if key['kid'] == kid: return self.jwt_key_to_pem(key) - raise DecodeError('Cannot find kid={}'.format(kid)) + raise DecodeError(f'Cannot find kid={kid}') def user_data(self, access_token, *args, **kwargs): response = kwargs.get('response') diff --git a/social_core/backends/azuread_tenant.py b/social_core/backends/azuread_tenant.py index 53b6501c..c17f8bc1 100644 --- a/social_core/backends/azuread_tenant.py +++ b/social_core/backends/azuread_tenant.py @@ -79,7 +79,7 @@ def get_certificate(self, kid): x5c = key['x5c'][0] break else: - raise DecodeError('Cannot find kid={}'.format(kid)) + raise DecodeError(f'Cannot find kid={kid}') return load_der_x509_certificate(base64.b64decode(x5c), default_backend()) diff --git a/social_core/backends/beats.py b/social_core/backends/beats.py index 4dda471a..61de4038 100644 --- a/social_core/backends/beats.py +++ b/social_core/backends/beats.py @@ -23,8 +23,8 @@ def get_user_id(self, details, response): def auth_headers(self): return { - 'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode( - ('{0}:{1}'.format(*self.get_key_and_secret()).encode()) + 'Authorization': 'Basic {}'.format(base64.urlsafe_b64encode( + '{}:{}'.format(*self.get_key_and_secret()).encode() )) } @@ -61,5 +61,5 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( 'https://partner.api.beatsmusic.com/v1/api/me', - headers={'Authorization': 'Bearer {0}'.format(access_token)} + headers={'Authorization': f'Bearer {access_token}'} ) diff --git a/social_core/backends/chatwork.py b/social_core/backends/chatwork.py index b69a627e..82d71f05 100644 --- a/social_core/backends/chatwork.py +++ b/social_core/backends/chatwork.py @@ -23,12 +23,12 @@ class ChatworkOAuth2(BaseOAuth2): def api_url(self, path): api_url = self.setting('API_URL') or self.API_URL - return '{0}{1}'.format(api_url.rstrip('/'), path) + return '{}{}'.format(api_url.rstrip('/'), path) def auth_headers(self): return { 'Authorization': b'Basic ' + base64.b64encode( - '{0}:{1}'.format(*self.get_key_and_secret()).encode() + '{}:{}'.format(*self.get_key_and_secret()).encode() ) } diff --git a/social_core/backends/coding.py b/social_core/backends/coding.py index 81f998b1..5bbf138c 100644 --- a/social_core/backends/coding.py +++ b/social_core/backends/coding.py @@ -43,6 +43,6 @@ def user_data(self, access_token, *args, **kwargs): def _user_data(self, access_token, path=None): url = urljoin( self.api_url(), - 'account/current_user{0}'.format(path or '') + 'account/current_user{}'.format(path or '') ) return self.get_json(url, params={'access_token': access_token}) diff --git a/social_core/backends/cognito.py b/social_core/backends/cognito.py index dabe718b..df6f33af 100644 --- a/social_core/backends/cognito.py +++ b/social_core/backends/cognito.py @@ -12,13 +12,13 @@ def user_pool_domain(self): return self.setting('POOL_DOMAIN') def authorization_url(self): - return '{}/login'.format(self.user_pool_domain()) + return f'{self.user_pool_domain()}/login' def access_token_url(self): - return '{}/oauth2/token'.format(self.user_pool_domain()) + return f'{self.user_pool_domain()}/oauth2/token' def user_data_url(self): - return '{}/oauth2/userInfo'.format(self.user_pool_domain()) + return f'{self.user_pool_domain()}/oauth2/userInfo' def get_user_details(self, response): """Return user details from their cognito pool account""" @@ -38,7 +38,7 @@ def user_data(self, access_token, *args, **kwargs): """Grab user profile information from cognito.""" response = self.get_json( url=self.user_data_url(), - headers={'Authorization': 'Bearer {}'.format(access_token)}, + headers={'Authorization': f'Bearer {access_token}'}, ) user_data = { diff --git a/social_core/backends/coursera.py b/social_core/backends/coursera.py index ff5f287a..56162bf6 100644 --- a/social_core/backends/coursera.py +++ b/social_core/backends/coursera.py @@ -40,4 +40,4 @@ def user_data(self, access_token, *args, **kwargs): ) def get_auth_header(self, access_token): - return {'Authorization': 'Bearer {0}'.format(access_token)} + return {'Authorization': f'Bearer {access_token}'} diff --git a/social_core/backends/discord.py b/social_core/backends/discord.py index 24cd847f..25d09127 100644 --- a/social_core/backends/discord.py +++ b/social_core/backends/discord.py @@ -1,31 +1,31 @@ -""" -Discord Auth OAuth2 backend, docs at: - https://discord.com/developers/docs/topics/oauth2 -""" -from .oauth import BaseOAuth2 - - -class DiscordOAuth2(BaseOAuth2): - name = 'discord' - HOSTNAME = 'discord.com' - AUTHORIZATION_URL = 'https://%s/api/oauth2/authorize' % HOSTNAME - ACCESS_TOKEN_URL = 'https://%s/api/oauth2/token' % HOSTNAME - ACCESS_TOKEN_METHOD = 'POST' - REVOKE_TOKEN_URL = 'https://%s/api/oauth2/token/revoke' % HOSTNAME - REVOKE_TOKEN_METHOD = 'GET' - DEFAULT_SCOPE = ['identify'] - SCOPE_SEPARATOR = '+' - REDIRECT_STATE = False - EXTRA_DATA = [ - ('expires_in', 'expires'), - ('refresh_token', 'refresh_token') - ] - - def get_user_details(self, response): - return {'username': response.get('username'), - 'email': response.get('email') or ''} - - def user_data(self, access_token, *args, **kwargs): - url = 'https://%s/api/users/@me' % self.HOSTNAME - auth_header = {'Authorization': 'Bearer %s' % access_token} - return self.get_json(url, headers=auth_header) +""" +Discord Auth OAuth2 backend, docs at: + https://discord.com/developers/docs/topics/oauth2 +""" +from .oauth import BaseOAuth2 + + +class DiscordOAuth2(BaseOAuth2): + name = 'discord' + HOSTNAME = 'discord.com' + AUTHORIZATION_URL = 'https://%s/api/oauth2/authorize' % HOSTNAME + ACCESS_TOKEN_URL = 'https://%s/api/oauth2/token' % HOSTNAME + ACCESS_TOKEN_METHOD = 'POST' + REVOKE_TOKEN_URL = 'https://%s/api/oauth2/token/revoke' % HOSTNAME + REVOKE_TOKEN_METHOD = 'GET' + DEFAULT_SCOPE = ['identify'] + SCOPE_SEPARATOR = '+' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('expires_in', 'expires'), + ('refresh_token', 'refresh_token') + ] + + def get_user_details(self, response): + return {'username': response.get('username'), + 'email': response.get('email') or ''} + + def user_data(self, access_token, *args, **kwargs): + url = 'https://%s/api/users/@me' % self.HOSTNAME + auth_header = {'Authorization': 'Bearer %s' % access_token} + return self.get_json(url, headers=auth_header) diff --git a/social_core/backends/discourse.py b/social_core/backends/discourse.py index 4c8bdc4f..905392b1 100644 --- a/social_core/backends/discourse.py +++ b/social_core/backends/discourse.py @@ -39,7 +39,7 @@ def auth_url(self): 'sso': base_64_payload, 'sig': payload_signature }) - return '{0}?{1}'.format(self.get_idp_url(), encoded_params) + return f'{self.get_idp_url()}?{encoded_params}' def get_idp_url(self): return self.setting('SERVER_URL') + '/session/sso_provider' diff --git a/social_core/backends/douban.py b/social_core/backends/douban.py index ff931c0a..96a07102 100644 --- a/social_core/backends/douban.py +++ b/social_core/backends/douban.py @@ -55,5 +55,5 @@ def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( 'https://api.douban.com/v2/user/~me', - headers={'Authorization': 'Bearer {0}'.format(access_token)} + headers={'Authorization': f'Bearer {access_token}'} ) diff --git a/social_core/backends/dribbble.py b/social_core/backends/dribbble.py index 7381e6d4..d8835376 100644 --- a/social_core/backends/dribbble.py +++ b/social_core/backends/dribbble.py @@ -58,5 +58,5 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://api.dribbble.com/v1/user', headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) diff --git a/social_core/backends/dropbox.py b/social_core/backends/dropbox.py index a22b79b1..7a3addb2 100644 --- a/social_core/backends/dropbox.py +++ b/social_core/backends/dropbox.py @@ -26,6 +26,6 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( 'https://api.dropboxapi.com/2/users/get_current_account', - headers={'Authorization': 'Bearer {0}'.format(access_token)}, + headers={'Authorization': f'Bearer {access_token}'}, method='POST' ) diff --git a/social_core/backends/eveonline.py b/social_core/backends/eveonline.py index 1b7e5dc1..33b15be3 100644 --- a/social_core/backends/eveonline.py +++ b/social_core/backends/eveonline.py @@ -38,5 +38,5 @@ def user_data(self, access_token, *args, **kwargs): """Get Character data from EVE server""" return self.get_json( 'https://login.eveonline.com/oauth/verify', - headers={'Authorization': 'Bearer {0}'.format(access_token)} + headers={'Authorization': f'Bearer {access_token}'} ) diff --git a/social_core/backends/exacttarget.py b/social_core/backends/exacttarget.py index a066cdde..5beeacc7 100644 --- a/social_core/backends/exacttarget.py +++ b/social_core/backends/exacttarget.py @@ -45,7 +45,7 @@ def get_user_id(self, details, response): 'email': 'example@example.com' } """ - return '{0}'.format(details.get('id')) + return '{}'.format(details.get('id')) def uses_redirect(self): return False diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index 11f70dd1..c8ef724f 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -14,7 +14,7 @@ AuthMissingParameter -API_VERSION = 8.0 +API_VERSION = 14.0 class FacebookOAuth2(BaseOAuth2): @@ -65,7 +65,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - params = self.setting('PROFILE_EXTRA_PARAMS', {}) + params = self.setting('PROFILE_EXTRA_PARAMS', {}).copy() params['access_token'] = access_token if self.setting('APPSECRET_PROOF', True): @@ -213,7 +213,7 @@ def auth_html(self): def load_signed_request(self, signed_request): def base64_url_decode(data): data = data.encode('ascii') - data += '='.encode('ascii') * (4 - (len(data) % 4)) + data += b'=' * (4 - (len(data) % 4)) return base64.urlsafe_b64decode(data) key, secret = self.get_key_and_secret() diff --git a/social_core/backends/fitbit.py b/social_core/backends/fitbit.py index a2f4cde3..768cc8f1 100644 --- a/social_core/backends/fitbit.py +++ b/social_core/backends/fitbit.py @@ -59,9 +59,9 @@ def user_data(self, access_token, *args, **kwargs): )['user'] def auth_headers(self): - tokens = '{0}:{1}'.format(*self.get_key_and_secret()) + tokens = '{}:{}'.format(*self.get_key_and_secret()) tokens = base64.urlsafe_b64encode(tokens.encode()) tokens = tokens.decode() return { - 'Authorization': 'Basic {0}'.format(tokens) + 'Authorization': f'Basic {tokens}' } diff --git a/social_core/backends/gae.py b/social_core/backends/gae.py index a6aa8e3e..1919db01 100644 --- a/social_core/backends/gae.py +++ b/social_core/backends/gae.py @@ -1,7 +1,6 @@ """ Google App Engine support using User API """ -from __future__ import absolute_import from google.appengine.api import users diff --git a/social_core/backends/gitea.py b/social_core/backends/gitea.py index 1d19a492..5899f42d 100644 --- a/social_core/backends/gitea.py +++ b/social_core/backends/gitea.py @@ -24,7 +24,7 @@ class GiteaOAuth2 (BaseOAuth2): def api_url(self, path): api_url = self.setting('API_URL') or self.API_URL - return '{0}{1}'.format(api_url.rstrip('/'), path) + return '{}{}'.format(api_url.rstrip('/'), path) def authorization_url(self): return self.api_url('/login/oauth/authorize') diff --git a/social_core/backends/github.py b/social_core/backends/github.py index 42a8dac6..e8bd2d9e 100644 --- a/social_core/backends/github.py +++ b/social_core/backends/github.py @@ -64,8 +64,8 @@ def user_data(self, access_token, *args, **kwargs): return data def _user_data(self, access_token, path=None): - url = urljoin(self.api_url(), 'user{0}'.format(path or '')) - return self.get_json(url, headers={'Authorization': 'token {0}'.format(access_token)}) + url = urljoin(self.api_url(), 'user{}'.format(path or '')) + return self.get_json(url, headers={'Authorization': f'token {access_token}'}) class GithubMemberOAuth2(GithubOAuth2): @@ -74,7 +74,7 @@ class GithubMemberOAuth2(GithubOAuth2): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" user_data = super().user_data(access_token, *args, **kwargs) - headers = {'Authorization': 'token {0}'.format(access_token)} + headers = {'Authorization': f'token {access_token}'} try: self.request(self.member_url(user_data), headers=headers) except HTTPError as err: diff --git a/social_core/backends/gitlab.py b/social_core/backends/gitlab.py index 13f51500..3fc7f07b 100644 --- a/social_core/backends/gitlab.py +++ b/social_core/backends/gitlab.py @@ -27,7 +27,7 @@ class GitLabOAuth2(BaseOAuth2): def api_url(self, path): api_url = self.setting('API_URL') or self.API_URL - return '{0}{1}'.format(api_url.rstrip('/'), path) + return '{}{}'.format(api_url.rstrip('/'), path) def authorization_url(self): return self.api_url('/oauth/authorize') diff --git a/social_core/backends/globus.py b/social_core/backends/globus.py index a9368081..818f0fbf 100644 --- a/social_core/backends/globus.py +++ b/social_core/backends/globus.py @@ -10,6 +10,7 @@ class GlobusOpenIdConnect(OpenIdConnectAuth): name = 'globus' OIDC_ENDPOINT = 'https://auth.globus.org' + JWT_ALGORITHMS = ['RS256', 'RS512'] EXTRA_DATA = [ ('expires_in', 'expires_in', True), ('refresh_token', 'refresh_token', True), diff --git a/social_core/backends/goclio.py b/social_core/backends/goclio.py index 4d75c1ca..e682e9da 100644 --- a/social_core/backends/goclio.py +++ b/social_core/backends/goclio.py @@ -16,7 +16,7 @@ def get_user_details(self, response): email = user.get('email', None) first_name, last_name = (user.get('first_name', None), user.get('last_name', None)) - fullname = '%s %s' % (first_name, last_name) + fullname = f'{first_name} {last_name}' return {'username': username, 'fullname': fullname, diff --git a/social_core/backends/itembase.py b/social_core/backends/itembase.py index 7938e932..d8330b40 100644 --- a/social_core/backends/itembase.py +++ b/social_core/backends/itembase.py @@ -50,14 +50,14 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): return self.get_json(self.USER_DETAILS_URL, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) def activation_data(self, response): # returns activation_data dict with activation_url inside # see http://developers.itembase.com/authentication/activation return self.get_json(self.ACTIVATION_ENDPOINT, headers={ - 'Authorization': 'Bearer {0}'.format(response['access_token']) + 'Authorization': 'Bearer {}'.format(response['access_token']) }) @handle_http_errors diff --git a/social_core/backends/jawbone.py b/social_core/backends/jawbone.py index 9805b5f0..c98d71ef 100644 --- a/social_core/backends/jawbone.py +++ b/social_core/backends/jawbone.py @@ -48,7 +48,7 @@ def process_error(self, data): if error == 'access_denied': raise AuthCanceled(self) else: - raise AuthUnknownError(self, 'Jawbone error was {0}'.format( + raise AuthUnknownError(self, 'Jawbone error was {}'.format( error )) return super().process_error(data) diff --git a/social_core/backends/justgiving.py b/social_core/backends/justgiving.py index 57c89a52..66de2b36 100644 --- a/social_core/backends/justgiving.py +++ b/social_core/backends/justgiving.py @@ -31,7 +31,7 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" key, secret = self.get_key_and_secret() return self.get_json(self.USER_DATA_URL, headers={ - 'Authorization': 'Bearer {0}'.format(access_token), + 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json', 'x-application-key': secret, 'x-api-key': key diff --git a/social_core/backends/kakao.py b/social_core/backends/kakao.py index ceea26fc..9e015eac 100644 --- a/social_core/backends/kakao.py +++ b/social_core/backends/kakao.py @@ -39,7 +39,7 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://kapi.kakao.com/v2/user/me', headers={ - 'Authorization': 'Bearer {0}'.format(access_token), + 'Authorization': f'Bearer {access_token}', 'Content_Type': 'application/x-www-form-urlencoded;charset=utf-8', }, params={'access_token': access_token} diff --git a/social_core/backends/line.py b/social_core/backends/line.py index b1cfcf4b..9cb0a527 100644 --- a/social_core/backends/line.py +++ b/social_core/backends/line.py @@ -96,7 +96,7 @@ def user_data(self, access_token, *args, **kwargs): response = self.get_json( self.USER_INFO_URL, headers={ - 'Authorization': 'Bearer {}'.format(access_token) + 'Authorization': f'Bearer {access_token}' } ) self.process_error(response) diff --git a/social_core/backends/livejournal.py b/social_core/backends/livejournal.py index 3e1df99d..795f8f97 100644 --- a/social_core/backends/livejournal.py +++ b/social_core/backends/livejournal.py @@ -23,4 +23,4 @@ def openid_url(self): """Returns LiveJournal authentication URL""" if not self.data.get('openid_lj_user'): raise AuthMissingParameter(self, 'openid_lj_user') - return 'http://{0}.livejournal.com'.format(self.data['openid_lj_user']) + return 'http://{}.livejournal.com'.format(self.data['openid_lj_user']) diff --git a/social_core/backends/loginradius.py b/social_core/backends/loginradius.py index 49bfa55d..bdea3a6e 100644 --- a/social_core/backends/loginradius.py +++ b/social_core/backends/loginradius.py @@ -65,5 +65,5 @@ def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server response. Since LoginRadius handles multiple providers, we need to distinguish them to prevent conflicts.""" - return '{0}-{1}'.format(response.get('Provider'), + return '{}-{}'.format(response.get('Provider'), response.get(self.ID_KEY)) diff --git a/social_core/backends/lyft.py b/social_core/backends/lyft.py index 8c016b45..9e259b26 100644 --- a/social_core/backends/lyft.py +++ b/social_core/backends/lyft.py @@ -37,7 +37,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json(self.USER_DATA_URL, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) def auth_complete_params(self, state=None): diff --git a/social_core/backends/mapmyfitness.py b/social_core/backends/mapmyfitness.py index 7c202b9b..7311bdd2 100644 --- a/social_core/backends/mapmyfitness.py +++ b/social_core/backends/mapmyfitness.py @@ -43,7 +43,7 @@ def user_data(self, access_token, *args, **kwargs): key = self.get_key_and_secret()[0] url = 'https://oauth2-api.mapmyapi.com/v7.0/user/self/' headers = { - 'Authorization': 'Bearer {0}'.format(access_token), + 'Authorization': f'Bearer {access_token}', 'Api-Key': key } return self.get_json(url, headers=headers) diff --git a/social_core/backends/mediawiki.py b/social_core/backends/mediawiki.py index 16565658..f8a5b1ce 100644 --- a/social_core/backends/mediawiki.py +++ b/social_core/backends/mediawiki.py @@ -72,7 +72,7 @@ def oauth_authorization_request(self, token): state = self.get_or_create_state() base_url = self.setting('MEDIAWIKI_URL') - return '{0}?{1}'.format(base_url, urlencode({ + return '{}?{}'.format(base_url, urlencode({ 'title': 'Special:Oauth/authenticate', self.OAUTH_TOKEN_PARAMETER_NAME: oauth_token, self.REDIRECT_URI_PARAMETER_NAME: self.get_redirect_uri(state) @@ -123,7 +123,7 @@ def get_user_details(self, response): raise AuthException( self, 'An error occurred while trying to read json ' + - 'content: {0}'.format(exception) + f'content: {exception}' ) issuer = urlparse(identity['iss']).netloc @@ -132,7 +132,7 @@ def get_user_details(self, response): if not issuer == expected_domain: raise AuthException( self, - 'Unexpected issuer {0}, expected {1}'.format( + 'Unexpected issuer {}, expected {}'.format( issuer, expected_domain ) @@ -143,7 +143,7 @@ def get_user_details(self, response): if not now >= (issued_at - self.LEEWAY): raise AuthException( self, - 'Identity issued {0} seconds in the future'.format( + 'Identity issued {} seconds in the future'.format( issued_at - now ) ) @@ -157,7 +157,7 @@ def get_user_details(self, response): if identity['nonce'] != request_nonce: raise AuthException( self, - 'Replay attack detected: {0} != {1}'.format( + 'Replay attack detected: {} != {}'.format( identity['nonce'], request_nonce ) diff --git a/social_core/backends/mendeley.py b/social_core/backends/mendeley.py index ebb73e66..e1619109 100644 --- a/social_core/backends/mendeley.py +++ b/social_core/backends/mendeley.py @@ -63,5 +63,5 @@ def get_user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( 'https://api.mendeley.com/profiles/me/', - headers={'Authorization': 'Bearer {0}'.format(access_token)} + headers={'Authorization': f'Bearer {access_token}'} ) diff --git a/social_core/backends/monzo.py b/social_core/backends/monzo.py index 46b5ac63..c9b42706 100644 --- a/social_core/backends/monzo.py +++ b/social_core/backends/monzo.py @@ -28,5 +28,5 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://api.monzo.com/accounts', - headers={'Authorization': 'Bearer {0}'.format(access_token)}, + headers={'Authorization': f'Bearer {access_token}'}, ) diff --git a/social_core/backends/naver.py b/social_core/backends/naver.py index 332f8132..df0ad295 100644 --- a/social_core/backends/naver.py +++ b/social_core/backends/naver.py @@ -27,7 +27,7 @@ def user_data(self, access_token, *args, **kwargs): response = self.request( 'https://openapi.naver.com/v1/nid/me', headers={ - 'Authorization': 'Bearer {0}'.format(access_token), + 'Authorization': f'Bearer {access_token}', 'Content_Type': 'text/json' } ) diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index a2228244..c1b03b79 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -209,7 +209,7 @@ def get_unauthorized_token(self): utoken = parse_qs(utoken) if utoken.get(self.OAUTH_TOKEN_PARAMETER_NAME) == data_token: self.strategy.session_set(name, list(set(unauthed_tokens) - - set([orig_utoken]))) + {orig_utoken})) token = utoken break else: @@ -258,7 +258,7 @@ def oauth_authorization_request(self, token): ) state = self.get_or_create_state() params[self.REDIRECT_URI_PARAMETER_NAME] = self.get_redirect_uri(state) - return '{0}?{1}'.format(self.authorization_url(), urlencode(params)) + return f'{self.authorization_url()}?{urlencode(params)}' def oauth_auth(self, token=None, oauth_verifier=None, signature_type=SIGNATURE_TYPE_AUTH_HEADER): @@ -329,7 +329,7 @@ def auth_url(self): # redirect_uri matching is strictly enforced, so match the # providers value exactly. params = unquote(params) - return '{0}?{1}'.format(self.authorization_url(), params) + return f'{self.authorization_url()}?{params}' def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() diff --git a/social_core/backends/odnoklassniki.py b/social_core/backends/odnoklassniki.py index 03979c91..2b8c3848 100644 --- a/social_core/backends/odnoklassniki.py +++ b/social_core/backends/odnoklassniki.py @@ -51,8 +51,8 @@ class OdnoklassnikiApp(BaseAuth): ID_KEY = 'uid' def extra_data(self, user, uid, response, details=None, *args, **kwargs): - return dict([(key, value) for key, value in response.items() - if key in response['extra_data_list']]) + return {key: value for key, value in response.items() + if key in response['extra_data_list']} def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( @@ -75,7 +75,7 @@ def auth_complete(self, *args, **kwargs): self.setting('EXTRA_USER_DATA_LIST', ()) data = { 'method': 'users.getInfo', - 'uids': '{0}'.format(response['logged_user_id']), + 'uids': '{}'.format(response['logged_user_id']), 'fields': ','.join(fields), } client_key, client_secret = self.get_key_and_secret() @@ -100,7 +100,7 @@ def auth_complete(self, *args, **kwargs): def get_auth_sig(self): secret_key = self.setting('SECRET') - hash_source = '{0:s}{1:s}{2:s}'.format(self.data['logged_user_id'], + hash_source = '{:s}{:s}{:s}'.format(self.data['logged_user_id'], self.data['session_key'], secret_key) return md5(hash_source.encode('utf-8')).hexdigest() @@ -109,8 +109,8 @@ def get_response(self): fields = ('logged_user_id', 'api_server', 'application_key', 'session_key', 'session_secret_key', 'authorized', 'apiconnection') - return dict((name, self.data[name]) for name in fields - if name in self.data) + return {name: self.data[name] for name in fields + if name in self.data} def verify_auth_sig(self): correct_key = self.get_auth_sig() @@ -127,12 +127,12 @@ def odnoklassniki_oauth_sig(data, client_secret): search for "little bit different way" """ suffix = md5( - '{0:s}{1:s}'.format(data['access_token'], + '{:s}{:s}'.format(data['access_token'], client_secret).encode('utf-8') ).hexdigest() - check_list = sorted(['{0:s}={1:s}'.format(key, value) + check_list = sorted(f'{key:s}={value:s}' for key, value in data.items() - if key != 'access_token']) + if key != 'access_token') return md5((''.join(check_list) + suffix).encode('utf-8')).hexdigest() @@ -143,8 +143,8 @@ def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): If API method requires session context, request is signed with session secret key. Otherwise it is signed with application secret key """ - param_list = sorted(['{0:s}={1:s}'.format(key, value) - for key, value in data.items()]) + param_list = sorted(f'{key:s}={value:s}' + for key, value in data.items()) return md5( (''.join(param_list) + client_secret_or_session_secret).encode('utf-8') ).hexdigest() diff --git a/social_core/backends/okta.py b/social_core/backends/okta.py index b228e21f..d37e5f1c 100644 --- a/social_core/backends/okta.py +++ b/social_core/backends/okta.py @@ -24,7 +24,7 @@ def _url(self, path): def oidc_config(self): return self.get_json( self._url( - './.well-known/openid-configuration?client_id={}'.format( + '/.well-known/openid-configuration?client_id={}'.format( self.setting('KEY') ) ) @@ -60,6 +60,6 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( self._url('v1/userinfo'), headers={ - 'Authorization': 'Bearer {}'.format(access_token), + 'Authorization': f'Bearer {access_token}', } ) diff --git a/social_core/backends/open_id.py b/social_core/backends/open_id.py index 38b217c6..063db2c6 100644 --- a/social_core/backends/open_id.py +++ b/social_core/backends/open_id.py @@ -239,7 +239,7 @@ def openid_request(self, params=None): return self.consumer().begin(url_add_parameters(self.openid_url(), params)) except DiscoveryFailure as err: - raise AuthException(self, 'OpenID discovery error: {0}'.format( + raise AuthException(self, 'OpenID discovery error: {}'.format( err )) diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py index c770b934..3e678870 100644 --- a/social_core/backends/open_id_connect.py +++ b/social_core/backends/open_id_connect.py @@ -143,12 +143,28 @@ def validate_claims(self, id_token): raise AuthTokenError(self, 'Incorrect id_token: nonce') def find_valid_key(self, id_token): - for key in self.get_jwks_keys(): - rsakey = jwk.construct(key) - message, encoded_sig = id_token.rsplit('.', 1) - decoded_sig = base64url_decode(encoded_sig.encode('utf-8')) - if rsakey.verify(message.encode('utf-8'), decoded_sig): - return key + kid = jwt.get_unverified_header(id_token).get('kid') + + keys = self.get_jwks_keys() + if kid is not None: + for key in keys: + if kid == key.get('kid'): + break + else: + # In case the key id is not found in the cached keys, just + # reload the JWKS keys. Ideally this should be done by + # invalidating the cache. + self.get_jwks_keys.invalidate() + keys = self.get_jwks_keys() + + for key in keys: + if kid is None or kid == key.get('kid'): + rsakey = jwk.construct(key) + message, encoded_sig = id_token.rsplit('.', 1) + decoded_sig = base64url_decode(encoded_sig.encode('utf-8')) + if rsakey.verify(message.encode('utf-8'), decoded_sig): + return key + return None def validate_and_return_id_token(self, id_token, access_token): """ @@ -199,7 +215,7 @@ def request_access_token(self, *args, **kwargs): def user_data(self, access_token, *args, **kwargs): return self.get_json(self.userinfo_url(), headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) def get_user_details(self, response): diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index fa68fd38..14ef280b 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -116,7 +116,7 @@ def user_data(self, access_token, *args, **kwargs): self.USER_ID_URL, headers={ 'Content-Type': 'application/json', - 'Authorization': 'Bearer {}'.format(str(access_token)) + 'Authorization': f'Bearer {str(access_token)}' }, ) diff --git a/social_core/backends/osso.py b/social_core/backends/osso.py index 0fcbe8d7..a7c8a5b3 100644 --- a/social_core/backends/osso.py +++ b/social_core/backends/osso.py @@ -44,7 +44,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads normalized user profile from Osso""" - url = '{osso_base_url}/oauth/me?'.format(osso_base_url=self.osso_base_url) + urlencode({ + url = f'{self.osso_base_url}/oauth/me?' + urlencode({ 'access_token': access_token }) return self.get_json(url) \ No newline at end of file diff --git a/social_core/backends/patreon.py b/social_core/backends/patreon.py index d0e0dc8d..ad5d8009 100644 --- a/social_core/backends/patreon.py +++ b/social_core/backends/patreon.py @@ -33,9 +33,9 @@ def user_data(self, access_token, *args, **kwargs): def get_api(self, access_token, suffix): return self.get_json( - 'https://www.patreon.com/api/oauth2/v2/{}'.format(suffix), + f'https://www.patreon.com/api/oauth2/v2/{suffix}', headers=self.get_auth_header(access_token) ) def get_auth_header(self, access_token): - return {'Authorization': 'Bearer {0}'.format(access_token)} + return {'Authorization': f'Bearer {access_token}'} diff --git a/social_core/backends/phabricator.py b/social_core/backends/phabricator.py index 1bd4ddc3..0a2eb9fe 100644 --- a/social_core/backends/phabricator.py +++ b/social_core/backends/phabricator.py @@ -16,7 +16,7 @@ class PhabricatorOAuth2(BaseOAuth2): def api_url(self, path): api_url = self.setting('API_URL') or self.API_URL - return '{0}{1}'.format(api_url.rstrip('/'), path) + return '{}{}'.format(api_url.rstrip('/'), path) def authorization_url(self): return self.api_url('/oauthserver/auth/') diff --git a/social_core/backends/pinterest.py b/social_core/backends/pinterest.py index eb5d2ce3..c28417d0 100644 --- a/social_core/backends/pinterest.py +++ b/social_core/backends/pinterest.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- """ Pinterest OAuth2 backend, docs at: https://developers.pinterest.com/docs/api/authentication/ """ -from __future__ import unicode_literals import ssl diff --git a/social_core/backends/pixelpin.py b/social_core/backends/pixelpin.py index 3048134b..31b9c41f 100644 --- a/social_core/backends/pixelpin.py +++ b/social_core/backends/pixelpin.py @@ -30,6 +30,6 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://login.pixelpin.io/connect/userinfo', headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' } ) diff --git a/social_core/backends/professionali.py b/social_core/backends/professionali.py index 15f39776..4e7e286c 100644 --- a/social_core/backends/professionali.py +++ b/social_core/backends/professionali.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Professionaly OAuth 2.0 support. @@ -26,9 +25,9 @@ def get_user_details(self, response): first_name, last_name = map(response.get, ('firstname', 'lastname')) email = '' if self.setting('FAKE_EMAIL'): - email = '{0}@professionali.ru'.format(time()) + email = f'{time()}@professionali.ru' return { - 'username': '{0}_{1}'.format(last_name, first_name), + 'username': f'{last_name}_{first_name}', 'first_name': first_name, 'last_name': last_name, 'email': email @@ -45,7 +44,7 @@ def user_data(self, access_token, response, *args, **kwargs): } try: return self.get_json(url, params)[0] - except (TypeError, KeyError, IOError, ValueError, IndexError): + except (TypeError, KeyError, OSError, ValueError, IndexError): return None def get_json(self, url, *args, **kwargs): diff --git a/social_core/backends/pushbullet.py b/social_core/backends/pushbullet.py index d821223f..d799fd1c 100644 --- a/social_core/backends/pushbullet.py +++ b/social_core/backends/pushbullet.py @@ -18,6 +18,6 @@ def get_user_details(self, response): return {'username': response.get('access_token')} def get_user_id(self, details, response): - auth = 'Basic {0}'.format(base64.b64encode(details['username'])) + auth = 'Basic {}'.format(base64.b64encode(details['username'])) return self.get_json('https://api.pushbullet.com/v2/users/me', headers={'Authorization': auth})['iden'] diff --git a/social_core/backends/qiita.py b/social_core/backends/qiita.py index 87fb6a40..e484448f 100644 --- a/social_core/backends/qiita.py +++ b/social_core/backends/qiita.py @@ -62,6 +62,6 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( 'https://qiita.com/api/v2/authenticated_user', headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' } ) diff --git a/social_core/backends/readability.py b/social_core/backends/readability.py index 593117fc..b5adf79b 100644 --- a/social_core/backends/readability.py +++ b/social_core/backends/readability.py @@ -12,9 +12,9 @@ class ReadabilityOAuth(BaseOAuth1): """Readability OAuth authentication backend""" name = 'readability' ID_KEY = 'username' - AUTHORIZATION_URL = '{0}/oauth/authorize/'.format(READABILITY_API) - REQUEST_TOKEN_URL = '{0}/oauth/request_token/'.format(READABILITY_API) - ACCESS_TOKEN_URL = '{0}/oauth/access_token/'.format(READABILITY_API) + AUTHORIZATION_URL = f'{READABILITY_API}/oauth/authorize/' + REQUEST_TOKEN_URL = f'{READABILITY_API}/oauth/request_token/' + ACCESS_TOKEN_URL = f'{READABILITY_API}/oauth/access_token/' EXTRA_DATA = [('date_joined', 'date_joined'), ('kindle_email_address', 'kindle_email_address'), ('avatar_url', 'avatar_url'), diff --git a/social_core/backends/reddit.py b/social_core/backends/reddit.py index 97fa76b6..acd8102b 100644 --- a/social_core/backends/reddit.py +++ b/social_core/backends/reddit.py @@ -43,7 +43,7 @@ def user_data(self, access_token, *args, **kwargs): def auth_headers(self): return { 'Authorization': b'Basic ' + base64.urlsafe_b64encode( - '{0}:{1}'.format(*self.get_key_and_secret()).encode() + '{}:{}'.format(*self.get_key_and_secret()).encode() ) } diff --git a/social_core/backends/runkeeper.py b/social_core/backends/runkeeper.py index 18c2eec8..2bd3776b 100644 --- a/social_core/backends/runkeeper.py +++ b/social_core/backends/runkeeper.py @@ -43,5 +43,5 @@ def user_data(self, access_token, *args, **kwargs): return dict(user_data, **profile_data) def _user_data(self, access_token, path): - url = 'https://api.runkeeper.com{0}'.format(path) + url = f'https://api.runkeeper.com{path}' return self.get_json(url, params={'access_token': access_token}) diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index c2902475..eb72e649 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -303,7 +303,7 @@ def get_user_id(self, details, response): """ idp = self.get_idp(response['idp_name']) uid = idp.get_user_permanent_id(response['attributes']) - return '{0}:{1}'.format(idp.name, uid) + return f'{idp.name}:{uid}' def auth_complete(self, *args, **kwargs): """ @@ -318,7 +318,7 @@ def auth_complete(self, *args, **kwargs): if errors or not auth.is_authenticated(): reason = auth.get_last_error_reason() raise AuthFailed( - self, 'SAML login failed: {0} ({1})'.format(errors, reason) + self, f'SAML login failed: {errors} ({reason})' ) attributes = auth.get_attributes() diff --git a/social_core/backends/shimmering.py b/social_core/backends/shimmering.py index bc9a18c0..17650cf7 100644 --- a/social_core/backends/shimmering.py +++ b/social_core/backends/shimmering.py @@ -18,7 +18,7 @@ def get_user_details(self, response): last_name = response.get('last_name') email = response.get('email') username = response.get('username') - fullname = '{} {}'.format(first_name, last_name) + fullname = f'{first_name} {last_name}' return { 'username': username, 'fullname': fullname, diff --git a/social_core/backends/shopify.py b/social_core/backends/shopify.py index 9c8967d1..9f64c36b 100644 --- a/social_core/backends/shopify.py +++ b/social_core/backends/shopify.py @@ -91,7 +91,7 @@ def do_auth(self, access_token, shop_url, website, *args, **kwargs): 'backend': self, 'response': { 'shop': shop_url, - 'website': 'http://{0}'.format(website), + 'website': f'http://{website}', 'access_token': access_token } }) diff --git a/social_core/backends/sketchfab.py b/social_core/backends/sketchfab.py index 5a8d0c6b..8b747a2a 100644 --- a/social_core/backends/sketchfab.py +++ b/social_core/backends/sketchfab.py @@ -35,5 +35,5 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json('https://sketchfab.com/v2/users/me', headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) diff --git a/social_core/backends/slack.py b/social_core/backends/slack.py index def02fbb..341654b2 100644 --- a/social_core/backends/slack.py +++ b/social_core/backends/slack.py @@ -3,7 +3,6 @@ https://python-social-auth.readthedocs.io/en/latest/backends/slack.html https://api.slack.com/docs/oauth """ -from __future__ import unicode_literals from .oauth import BaseOAuth2 @@ -42,7 +41,7 @@ def get_user_details(self, response): if self.setting('USERNAME_WITH_TEAM', True) and team and \ 'name' in team: - username = '{0}@{1}'.format(username, response['team']['name']) + username = '{}@{}'.format(username, response['team']['name']) return { 'username': username, diff --git a/social_core/backends/spotify.py b/social_core/backends/spotify.py index cc0e67e0..55c95a1e 100644 --- a/social_core/backends/spotify.py +++ b/social_core/backends/spotify.py @@ -22,10 +22,10 @@ class SpotifyOAuth2(BaseOAuth2): ] def auth_headers(self): - auth_str = '{0}:{1}'.format(*self.get_key_and_secret()) + auth_str = '{}:{}'.format(*self.get_key_and_secret()) b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode() return { - 'Authorization': 'Basic {0}'.format(b64_auth_str) + 'Authorization': f'Basic {b64_auth_str}' } def get_user_details(self, response): @@ -43,5 +43,5 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( 'https://api.spotify.com/v1/me', - headers={'Authorization': 'Bearer {0}'.format(access_token)} + headers={'Authorization': f'Bearer {access_token}'} ) diff --git a/social_core/backends/stripe.py b/social_core/backends/stripe.py index 54d3cbd1..fe2e0a2c 100644 --- a/social_core/backends/stripe.py +++ b/social_core/backends/stripe.py @@ -39,7 +39,7 @@ def auth_complete_params(self, state=None): def auth_headers(self): client_id, client_secret = self.get_key_and_secret() return {'Accept': 'application/json', - 'Authorization': 'Bearer {0}'.format(client_secret)} + 'Authorization': f'Bearer {client_secret}'} def refresh_token_params(self, refresh_token, *args, **kwargs): return {'refresh_token': refresh_token, diff --git a/social_core/backends/telegram.py b/social_core/backends/telegram.py index e7f5d599..2f5077c8 100644 --- a/social_core/backends/telegram.py +++ b/social_core/backends/telegram.py @@ -23,7 +23,7 @@ def verify_data(self, response): if received_hash_string is None or auth_date is None: raise AuthMissingParameter('telegram', 'hash or auth_date') - data_check_string = ['{}={}'.format(k, v) + data_check_string = [f'{k}={v}' for k, v in response.items() if k != 'hash'] data_check_string = '\n'.join(sorted(data_check_string)) secret_key = hashlib.sha256(bot_token.encode()).digest() @@ -43,7 +43,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): def get_user_details(self, response): first_name = response.get('first_name', '') last_name = response.get('last_name', '') - fullname = '{} {}'.format(first_name, last_name).strip() + fullname = f'{first_name} {last_name}'.strip() return { 'username': response.get('username') or response[self.ID_KEY], 'first_name': first_name, diff --git a/social_core/backends/twilio.py b/social_core/backends/twilio.py index 449c7726..756ad523 100644 --- a/social_core/backends/twilio.py +++ b/social_core/backends/twilio.py @@ -28,7 +28,7 @@ def auth_url(self): callback = self.strategy.absolute_uri(self.redirect_uri) callback = sub(r'^https', 'http', callback) query = urlencode({'cb': callback}) - return 'https://www.twilio.com/authorize/{0}?{1}'.format(key, query) + return f'https://www.twilio.com/authorize/{key}?{query}' def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" diff --git a/social_core/backends/twitch.py b/social_core/backends/twitch.py index aa555aae..a7222db2 100644 --- a/social_core/backends/twitch.py +++ b/social_core/backends/twitch.py @@ -3,6 +3,32 @@ https://python-social-auth.readthedocs.io/en/latest/backends/twitch.html """ from .oauth import BaseOAuth2 +from .open_id_connect import OpenIdConnectAuth + + +class TwitchOpenIdConnect(OpenIdConnectAuth): + """Twitch OpenID Connect authentication backend""" + name = 'twitch' + USERNAME_KEY = 'preferred_username' + OIDC_ENDPOINT = 'https://id.twitch.tv/oauth2' + DEFAULT_SCOPE = ['openid', 'user:read:email'] + TWITCH_CLAIMS = '{"id_token":{"email": null,"email_verified":null,"preferred_username":null}}' + + def auth_params(self, state=None): + params = super().auth_params(state) + # Twitch uses a non-compliant OpenID implementation where the claims must be passed as a param + params['claims'] = self.TWITCH_CLAIMS + return params + + def get_user_details(self, response): + return { + 'username': self.id_token['preferred_username'], + 'email': self.id_token['email'], + # Twitch does not provide this information + 'fullname': '', + 'first_name': '', + 'last_name': '', + } class TwitchOAuth2(BaseOAuth2): @@ -12,19 +38,31 @@ class TwitchOAuth2(BaseOAuth2): AUTHORIZATION_URL = 'https://id.twitch.tv/oauth2/authorize' ACCESS_TOKEN_URL = 'https://id.twitch.tv/oauth2/token' ACCESS_TOKEN_METHOD = 'POST' - DEFAULT_SCOPE = ['user_read'] + DEFAULT_SCOPE = ['user:read:email'] REDIRECT_STATE = False + def get_user_id(self, details, response): + """ + Use twitch user id as unique id + """ + return response.get('id') + def get_user_details(self, response): return { - 'username': response.get('name'), + 'username': response.get('login'), 'email': response.get('email'), 'first_name': '', 'last_name': '' } def user_data(self, access_token, *args, **kwargs): - return self.get_json( - 'https://api.twitch.tv/kraken/user/', - params={'oauth_token': access_token, 'api_version': 5}, - ) + client_id, _ = self.get_key_and_secret() + auth_headers = { + 'Authorization': 'Bearer %s' % access_token, + 'Client-Id': client_id + } + url = 'https://api.twitch.tv/helix/users' + + data = self.get_json(url, headers=auth_headers) + + return data['data'][0] if data.get('data') else {} diff --git a/social_core/backends/uber.py b/social_core/backends/uber.py index 21bd9c51..b2aab2b3 100644 --- a/social_core/backends/uber.py +++ b/social_core/backends/uber.py @@ -34,6 +34,6 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" response = kwargs.pop('response') return self.get_json('https://api.uber.com/v1/me', headers={ - 'Authorization': '{0} {1}'.format(response.get('token_type'), + 'Authorization': '{} {}'.format(response.get('token_type'), access_token) }) diff --git a/social_core/backends/universe.py b/social_core/backends/universe.py index c67e1c96..7869a801 100644 --- a/social_core/backends/universe.py +++ b/social_core/backends/universe.py @@ -31,5 +31,5 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json(self.USER_INFO_URL, headers={ - 'Authorization': 'Bearer {}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) diff --git a/social_core/backends/upwork.py b/social_core/backends/upwork.py index be68d0de..37f3e68a 100644 --- a/social_core/backends/upwork.py +++ b/social_core/backends/upwork.py @@ -22,7 +22,7 @@ def get_user_details(self, response): auth_user = response.get('auth_user', {}) first_name = auth_user.get('first_name') last_name = auth_user.get('last_name') - fullname = '{} {}'.format(first_name, last_name) + fullname = f'{first_name} {last_name}' profile_url = info.get('profile_url', '') username = profile_url.rsplit('/')[-1].replace('~', '') return { diff --git a/social_core/backends/utils.py b/social_core/backends/utils.py index 9fbe51e0..88f143d0 100644 --- a/social_core/backends/utils.py +++ b/social_core/backends/utils.py @@ -76,7 +76,7 @@ def user_backends_data(user, backends, storage): if user_is_authenticated(user): associated = storage.user.get_social_auth_for_user(user) not_associated = list(set(available) - - set(assoc.provider for assoc in associated)) + {assoc.provider for assoc in associated}) values['associated'] = associated values['not_associated'] = not_associated return values diff --git a/social_core/backends/vend.py b/social_core/backends/vend.py index 99e45633..1ba046cc 100644 --- a/social_core/backends/vend.py +++ b/social_core/backends/vend.py @@ -32,8 +32,8 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" prefix = kwargs['response']['domain_prefix'] - url = 'https://{0}.vendhq.com/api/users'.format(prefix) + url = f'https://{prefix}.vendhq.com/api/users' data = self.get_json(url, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }) return data['users'][0] if data.get('users') else {} diff --git a/social_core/backends/vk.py b/social_core/backends/vk.py index a3157b79..f751ec20 100644 --- a/social_core/backends/vk.py +++ b/social_core/backends/vk.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ VK.com OpenAPI, OAuth2 and Iframe application OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/vk.html diff --git a/social_core/backends/weibo.py b/social_core/backends/weibo.py index cb5dc908..da62dcd5 100644 --- a/social_core/backends/weibo.py +++ b/social_core/backends/weibo.py @@ -1,4 +1,3 @@ -# coding:utf-8 # author:hepochen@gmail.com https://github.com/hepochen """ Weibo OAuth2 backend, docs at: diff --git a/social_core/backends/weixin.py b/social_core/backends/weixin.py index f68beb56..381de304 100644 --- a/social_core/backends/weixin.py +++ b/social_core/backends/weixin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # author:duoduo3369@gmail.com https://github.com/duoduo369 """ Weixin OAuth2 backend diff --git a/social_core/backends/yahoo.py b/social_core/backends/yahoo.py index d6ff380e..68bd2d90 100644 --- a/social_core/backends/yahoo.py +++ b/social_core/backends/yahoo.py @@ -102,7 +102,7 @@ def user_data(self, access_token, *args, **kwargs): url = 'https://api.login.yahoo.com/openid/v1/userinfo' return self.get_json(url, headers={ - 'Authorization': 'Bearer {0}'.format(access_token) + 'Authorization': f'Bearer {access_token}' }, method='GET') @handle_http_errors diff --git a/social_core/backends/zoom.py b/social_core/backends/zoom.py index 3c68a725..e26b821b 100644 --- a/social_core/backends/zoom.py +++ b/social_core/backends/zoom.py @@ -54,7 +54,7 @@ def auth_complete_params(self, state=None): def auth_headers(self): return { 'Authorization': b'Basic ' + base64.urlsafe_b64encode( - '{0}:{1}'.format(*self.get_key_and_secret()).encode() + '{}:{}'.format(*self.get_key_and_secret()).encode() ) } diff --git a/social_core/exceptions.py b/social_core/exceptions.py index 461fefa2..d5b38ccf 100644 --- a/social_core/exceptions.py +++ b/social_core/exceptions.py @@ -8,14 +8,14 @@ def __init__(self, backend_name): self.backend_name = backend_name def __str__(self): - return 'Incorrect authentication service "{0}"'.format( + return 'Incorrect authentication service "{}"'.format( self.backend_name ) class MissingBackend(WrongBackend): def __str__(self): - return 'Missing backend "{0}" entry'.format(self.backend_name) + return f'Missing backend "{self.backend_name}" entry' class NotAllowedToDisconnect(SocialAuthBaseException): @@ -36,7 +36,7 @@ def __str__(self): msg = super().__str__() if msg == 'access_denied': return 'Authentication process was canceled' - return 'Authentication failed: {0}'.format(msg) + return f'Authentication failed: {msg}' class AuthCanceled(AuthException): @@ -48,7 +48,7 @@ def __init__(self, *args, **kwargs): def __str__(self): msg = super().__str__() if msg: - return 'Authentication process canceled: {0}'.format(msg) + return f'Authentication process canceled: {msg}' return 'Authentication process canceled' @@ -56,14 +56,14 @@ class AuthUnknownError(AuthException): """Unknown auth process error.""" def __str__(self): msg = super().__str__() - return 'An unknown error happened while authenticating {0}'.format(msg) + return f'An unknown error happened while authenticating {msg}' class AuthTokenError(AuthException): """Auth token error.""" def __str__(self): msg = super().__str__() - return 'Token error: {0}'.format(msg) + return f'Token error: {msg}' class AuthMissingParameter(AuthException): @@ -73,7 +73,7 @@ def __init__(self, backend, parameter, *args, **kwargs): super().__init__(backend, *args, **kwargs) def __str__(self): - return 'Missing needed parameter {0}'.format(self.parameter) + return f'Missing needed parameter {self.parameter}' class AuthStateMissing(AuthException): diff --git a/social_core/pipeline/user.py b/social_core/pipeline/user.py index ef45ec08..f6cf14df 100644 --- a/social_core/pipeline/user.py +++ b/social_core/pipeline/user.py @@ -69,8 +69,8 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): if user: return {'is_new': False} - fields = dict((name, kwargs.get(name, details.get(name))) - for name in backend.setting('USER_FIELDS', USER_FIELDS)) + fields = {name: kwargs.get(name, details.get(name)) + for name in backend.setting('USER_FIELDS', USER_FIELDS)} if not fields: return diff --git a/social_core/pipeline/utils.py b/social_core/pipeline/utils.py index 112aed45..767e242f 100644 --- a/social_core/pipeline/utils.py +++ b/social_core/pipeline/utils.py @@ -63,6 +63,6 @@ def partial_load(strategy, token): kwargs['user'] = strategy.storage.user.get_user(user) partial.args = [strategy.from_session_value(val) for val in args] - partial.kwargs = dict((key, strategy.from_session_value(val)) - for key, val in kwargs.items()) + partial.kwargs = {key: strategy.from_session_value(val) + for key, val in kwargs.items()} return partial diff --git a/social_core/storage.py b/social_core/storage.py index f48a7cd9..2a2a5cbd 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -224,10 +224,10 @@ def oids(cls, server_url, handle=None): kwargs = {'server_url': server_url} if handle is not None: kwargs['handle'] = handle - return sorted([ + return sorted(( (assoc.id, cls.openid_association(assoc)) for assoc in cls.get(**kwargs) - ], key=lambda x: x[1].issued, reverse=True) + ), key=lambda x: x[1].issued, reverse=True) @classmethod def openid_association(cls, assoc): diff --git a/social_core/store.py b/social_core/store.py index aa17223a..0851aed7 100644 --- a/social_core/store.py +++ b/social_core/store.py @@ -1,10 +1,6 @@ +import pickle import time -try: - import cPickle as pickle -except ImportError: - import pickle - from openid.store.interface import OpenIDStore as BaseOpenIDStore from openid.store.nonce import SKEW diff --git a/social_core/strategy.py b/social_core/strategy.py index 9d7a7e59..4c0d74d4 100644 --- a/social_core/strategy.py +++ b/social_core/strategy.py @@ -116,7 +116,7 @@ def random_string(self, length=12, chars=ALLOWED_CHARS): random.SystemRandom() except NotImplementedError: key = self.setting('SECRET_KEY', '') - seed = '{0}{1}{2}'.format(random.getstate(), time.time(), key) + seed = f'{random.getstate()}{time.time()}{key}' random.seed(hashlib.sha256(seed.encode()).digest()) return ''.join([random.choice(chars) for i in range(length)]) diff --git a/social_core/tests/backends/legacy.py b/social_core/tests/backends/legacy.py index 45443cea..8393d3ba 100644 --- a/social_core/tests/backends/legacy.py +++ b/social_core/tests/backends/legacy.py @@ -13,14 +13,14 @@ class BaseLegacyTest(BaseBackendTest): def setUp(self): super().setUp() self.strategy.set_settings({ - 'SOCIAL_AUTH_{0}_FORM_URL'.format(self.name): - self.strategy.build_absolute_uri('/login/{0}'.format( + f'SOCIAL_AUTH_{self.name}_FORM_URL': + self.strategy.build_absolute_uri('/login/{}'.format( self.backend.name)) }) def extra_settings(self): - return {'SOCIAL_AUTH_{0}_FORM_URL'.format(self.name): - '/login/{0}'.format(self.backend.name)} + return {f'SOCIAL_AUTH_{self.name}_FORM_URL': + f'/login/{self.backend.name}'} def do_start(self): start_url = self.strategy.build_absolute_uri(self.backend.start().url) diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index 9058a8ed..0f04b2bd 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -58,11 +58,6 @@ def auth_handlers(self, start_url): target_url, status=200, body='foobar') - HTTPretty.register_uri(self._method(self.backend.ACCESS_TOKEN_METHOD), - uri=self.backend.access_token_url(), - status=self.access_token_status, - body=self.access_token_body or '', - content_type='text/json') if self.user_data_url: HTTPretty.register_uri(HTTPretty.POST if self.user_data_url_post else HTTPretty.GET, self.user_data_url, @@ -70,6 +65,13 @@ def auth_handlers(self, start_url): content_type=self.user_data_content_type) return target_url + def pre_complete_callback(self, start_url): + HTTPretty.register_uri(self._method(self.backend.ACCESS_TOKEN_METHOD), + uri=self.backend.access_token_url(), + status=self.access_token_status, + body=self.access_token_body or '', + content_type='text/json') + def do_start(self): start_url = self.backend.start().url target_url = self.auth_handlers(start_url) @@ -80,6 +82,7 @@ def do_start(self): self.backend) self.strategy.set_request_data(parse_qs(urlparse(target_url).query), self.backend) + self.pre_complete_callback(start_url) return self.backend.complete() diff --git a/social_core/tests/backends/open_id.py b/social_core/tests/backends/open_id.py index 8a4efdf3..75b75bc8 100644 --- a/social_core/tests/backends/open_id.py +++ b/social_core/tests/backends/open_id.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import sys from html.parser import HTMLParser diff --git a/social_core/tests/backends/test_auth0.py b/social_core/tests/backends/test_auth0.py index 3f40a18e..332f4535 100644 --- a/social_core/tests/backends/test_auth0.py +++ b/social_core/tests/backends/test_auth0.py @@ -1,4 +1,3 @@ - import json from jose import jwt @@ -43,7 +42,7 @@ class Auth0OAuth2Test(OAuth2Test): 'name': 'John Doe', 'picture': 'http://example.com/image.png', 'sub': '123456', - 'iss': 'https://{}/'.format(DOMAIN), + 'iss': f'https://{DOMAIN}/', }, JWK_KEY, algorithm='RS256') }) expected_username = 'foobar' diff --git a/social_core/tests/backends/test_bitbucket.py b/social_core/tests/backends/test_bitbucket.py index 263f6419..14818f93 100644 --- a/social_core/tests/backends/test_bitbucket.py +++ b/social_core/tests/backends/test_bitbucket.py @@ -13,41 +13,41 @@ class BitbucketOAuthMixin: bb_api_user_emails = 'https://api.bitbucket.org/2.0/user/emails' user_data_body = json.dumps({ - u'created_on': u'2012-03-29T18:07:38+00:00', - u'display_name': u'Foo Bar', - u'links': { - u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'}, - u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'}, - u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'}, - u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'}, - u'html': {u'href': u'https://bitbucket.org/foobar'}, - u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'}, - u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}}, - u'location': u'Fooville, Bar', - u'type': u'user', - u'username': u'foobar', - u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}', - u'website': u'http://foobar.com' + 'created_on': '2012-03-29T18:07:38+00:00', + 'display_name': 'Foo Bar', + 'links': { + 'avatar': {'href': 'https://bitbucket.org/account/foobar/avatar/32/'}, + 'followers': {'href': 'https://api.bitbucket.org/2.0/users/foobar/followers'}, + 'following': {'href': 'https://api.bitbucket.org/2.0/users/foobar/following'}, + 'hooks': {'href': 'https://api.bitbucket.org/2.0/users/foobar/hooks'}, + 'html': {'href': 'https://bitbucket.org/foobar'}, + 'repositories': {'href': 'https://api.bitbucket.org/2.0/repositories/foobar'}, + 'self': {'href': 'https://api.bitbucket.org/2.0/users/foobar'}}, + 'location': 'Fooville, Bar', + 'type': 'user', + 'username': 'foobar', + 'uuid': '{397621dc-0f78-329f-8d6d-727396248e3f}', + 'website': 'http://foobar.com' }) emails_body = json.dumps({ - u'page': 1, - u'pagelen': 10, - u'size': 2, - u'values': [ + 'page': 1, + 'pagelen': 10, + 'size': 2, + 'values': [ { - u'email': u'foo@bar.com', - u'is_confirmed': True, - u'is_primary': True, - u'links': {u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, - u'type': u'email' + 'email': 'foo@bar.com', + 'is_confirmed': True, + 'is_primary': True, + 'links': {'self': {'href': 'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + 'type': 'email' }, { - u'email': u'not@confirme.com', - u'is_confirmed': False, - u'is_primary': False, - u'links': {u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/not@confirmed.com'}}, - u'type': u'email' + 'email': 'not@confirme.com', + 'is_confirmed': False, + 'is_primary': False, + 'links': {'self': {'href': 'https://api.bitbucket.org/2.0/user/emails/not@confirmed.com'}}, + 'type': 'email' } ] }) @@ -82,16 +82,16 @@ def test_partial_pipeline(self): class BitbucketOAuth1FailTest(BitbucketOAuth1Test): emails_body = json.dumps({ - u'page': 1, - u'pagelen': 10, - u'size': 1, - u'values': [ + 'page': 1, + 'pagelen': 10, + 'size': 1, + 'values': [ { - u'email': u'foo@bar.com', - u'is_confirmed': False, - u'is_primary': True, - u'links': {u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, - u'type': u'email' + 'email': 'foo@bar.com', + 'is_confirmed': False, + 'is_primary': True, + 'links': {'self': {'href': 'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + 'type': 'email' } ] }) @@ -137,16 +137,16 @@ def test_partial_pipeline(self): class BitbucketOAuth2FailTest(BitbucketOAuth2Test): emails_body = json.dumps({ - u'page': 1, - u'pagelen': 10, - u'size': 1, - u'values': [ + 'page': 1, + 'pagelen': 10, + 'size': 1, + 'values': [ { - u'email': u'foo@bar.com', - u'is_confirmed': False, - u'is_primary': True, - u'links': {u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, - u'type': u'email' + 'email': 'foo@bar.com', + 'is_confirmed': False, + 'is_primary': True, + 'links': {'self': {'href': 'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + 'type': 'email' } ] }) diff --git a/social_core/tests/backends/test_discourse.py b/social_core/tests/backends/test_discourse.py index 458f8b9f..43524904 100644 --- a/social_core/tests/backends/test_discourse.py +++ b/social_core/tests/backends/test_discourse.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from urllib.parse import urlparse, parse_qs import requests @@ -36,9 +35,9 @@ def do_start(self): # NOTE: the signature was verified using the 'foo' key, like so: # hmac.new('foo', sso, sha256).hexdigest() sig = '04063f17c99a97b1a765c1e0d7bbb61afb8471d79a39ddcd6af5ba3c93eb10e1' - response_query_params = 'sso={0}&sig={1}'.format(sso, sig) + response_query_params = f'sso={sso}&sig={sig}' - response_url = '{0}?{1}'.format(return_url, response_query_params) + response_url = f'{return_url}?{response_query_params}' HTTPretty.register_uri( HTTPretty.GET, start_url, status=301, location=response_url ) @@ -50,9 +49,9 @@ def do_start(self): ) response = requests.get(start_url) - query_values = dict( - (k, v[0]) for k, v in parse_qs(urlparse(response.url).query).items() - ) + query_values = { + k: v[0] for k, v in parse_qs(urlparse(response.url).query).items() + } self.strategy.set_request_data(query_values, self.backend) return self.backend.complete() diff --git a/social_core/tests/backends/test_elixir.py b/social_core/tests/backends/test_elixir.py index 766c76be..28b726d8 100644 --- a/social_core/tests/backends/test_elixir.py +++ b/social_core/tests/backends/test_elixir.py @@ -1,5 +1,5 @@ from .oauth import OAuth2Test -from .open_id_connect import OpenIdConnectTestMixin +from .test_open_id_connect import OpenIdConnectTestMixin class ElixirOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): diff --git a/social_core/tests/backends/test_fence.py b/social_core/tests/backends/test_fence.py index 24a8362e..ed8b0aaf 100644 --- a/social_core/tests/backends/test_fence.py +++ b/social_core/tests/backends/test_fence.py @@ -1,7 +1,7 @@ import json from .oauth import OAuth2Test -from .open_id_connect import OpenIdConnectTestMixin +from .test_open_id_connect import OpenIdConnectTestMixin class FenceOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): diff --git a/social_core/tests/backends/test_globus.py b/social_core/tests/backends/test_globus.py index d9381bcc..443607e4 100644 --- a/social_core/tests/backends/test_globus.py +++ b/social_core/tests/backends/test_globus.py @@ -1,7 +1,7 @@ import json from .oauth import OAuth2Test -from .open_id_connect import OpenIdConnectTestMixin +from .test_open_id_connect import OpenIdConnectTestMixin class GlobusOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): diff --git a/social_core/tests/backends/test_google.py b/social_core/tests/backends/test_google.py index e3c7b58a..701af100 100644 --- a/social_core/tests/backends/test_google.py +++ b/social_core/tests/backends/test_google.py @@ -7,7 +7,7 @@ from ..models import User from .oauth import OAuth1Test, OAuth2Test -from .open_id_connect import OpenIdConnectTestMixin +from .test_open_id_connect import OpenIdConnectTestMixin class GoogleOAuth2Test(OAuth2Test): diff --git a/social_core/tests/backends/test_ngpvan.py b/social_core/tests/backends/test_ngpvan.py index 50e940e9..d0b13e46 100644 --- a/social_core/tests/backends/test_ngpvan.py +++ b/social_core/tests/backends/test_ngpvan.py @@ -170,12 +170,12 @@ def test_user_data(self): ] }) user = self.do_start() - self.assertEqual(user.username, u'testuser@user.local') - self.assertEqual(user.email, u'testuser@user.local') - self.assertEqual(user.extra_user_fields['phone'], u'+12015555555') - self.assertEqual(user.extra_user_fields['first_name'], u'John') - self.assertEqual(user.extra_user_fields['last_name'], u'Smith') - self.assertEqual(user.extra_user_fields['fullname'], u'John Smith') + self.assertEqual(user.username, 'testuser@user.local') + self.assertEqual(user.email, 'testuser@user.local') + self.assertEqual(user.extra_user_fields['phone'], '+12015555555') + self.assertEqual(user.extra_user_fields['first_name'], 'John') + self.assertEqual(user.extra_user_fields['last_name'], 'Smith') + self.assertEqual(user.extra_user_fields['fullname'], 'John Smith') def test_extra_data_phone(self): """Confirm that you can get a phone number via the relevant setting""" @@ -185,7 +185,7 @@ def test_extra_data_phone(self): ] }) user = self.do_start() - self.assertEqual(user.social_user.extra_data['phone'], u'+12015555555') + self.assertEqual(user.social_user.extra_data['phone'], '+12015555555') def test_association_uid(self): """Test that the correct association uid is stored in the database""" diff --git a/social_core/tests/backends/test_okta.py b/social_core/tests/backends/test_okta.py index 4762851c..16526cbd 100644 --- a/social_core/tests/backends/test_okta.py +++ b/social_core/tests/backends/test_okta.py @@ -2,7 +2,7 @@ from httpretty import HTTPretty from social_core.tests.backends.oauth import OAuth2Test -from social_core.tests.backends.open_id_connect import OpenIdConnectTestMixin +from social_core.tests.backends.test_open_id_connect import OpenIdConnectTestMixin JWK_KEY = { 'kty': 'RSA', diff --git a/social_core/tests/backends/open_id_connect.py b/social_core/tests/backends/test_open_id_connect.py similarity index 72% rename from social_core/tests/backends/open_id_connect.py rename to social_core/tests/backends/test_open_id_connect.py index 6731039e..928571f3 100644 --- a/social_core/tests/backends/open_id_connect.py +++ b/social_core/tests/backends/test_open_id_connect.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ...exceptions import AuthTokenError import os import sys @@ -6,10 +5,16 @@ import datetime import base64 from calendar import timegm +from unittest import mock +from urllib.parse import urlparse from jose import jwt from httpretty import HTTPretty +from social_core.backends.open_id_connect import OpenIdConnectAuth +from ...utils import parse_qs +from .oauth import OAuth2Test + sys.path.insert(0, '..') @@ -49,6 +54,7 @@ class OpenIdConnectTestMixin: issuer = None # id_token issuer openid_config_body = None key = None + access_token_kwargs = {} def setUp(self): super().setUp() @@ -73,9 +79,9 @@ def jwks(_request, _uri, headers): def extra_settings(self): settings = super().extra_settings() settings.update({ - 'SOCIAL_AUTH_{0}_KEY'.format(self.name): self.client_key, - 'SOCIAL_AUTH_{0}_SECRET'.format(self.name): self.client_secret, - 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): + f'SOCIAL_AUTH_{self.name}_KEY': self.client_key, + f'SOCIAL_AUTH_{self.name}_SECRET': self.client_secret, + f'SOCIAL_AUTH_{self.name}_ID_TOKEN_DECRYPTION_KEY': self.client_secret }) return settings @@ -96,7 +102,7 @@ def get_id_token(self, client_key=None, expiration_datetime=None, } def prepare_access_token_body(self, client_key=None, tamper_message=False, - expiration_datetime=None, + expiration_datetime=None, kid=None, issue_datetime=None, nonce=None, issuer=None): """ @@ -125,12 +131,13 @@ def prepare_access_token_body(self, client_key=None, tamper_message=False, ) body['id_token'] = jwt.encode( - id_token, + claims=id_token, key=dict(self.key, iat=timegm(issue_datetime.utctimetuple()), nonce=nonce), algorithm='RS256', - access_token='foobar' + access_token='foobar', + headers=dict(kid=kid), ) if tamper_message: @@ -142,12 +149,20 @@ def prepare_access_token_body(self, client_key=None, tamper_message=False, return json.dumps(body) def authtoken_raised(self, expected_message, **access_token_kwargs): - self.access_token_body = self.prepare_access_token_body( - **access_token_kwargs - ) + self.access_token_kwargs = access_token_kwargs with self.assertRaisesRegex(AuthTokenError, expected_message): self.do_login() + def pre_complete_callback(self, start_url): + nonce = parse_qs(urlparse(start_url).query)['nonce'] + + self.access_token_kwargs.setdefault('nonce', nonce) + self.access_token_body = self.prepare_access_token_body( + **self.access_token_kwargs + ) + super().pre_complete_callback(start_url) + + def test_invalid_signature(self): self.authtoken_raised( 'Token error: Signature verification failed', @@ -177,5 +192,41 @@ def test_invalid_issue_time(self): def test_invalid_nonce(self): self.authtoken_raised( 'Token error: Incorrect id_token: nonce', - nonce='something-wrong' + nonce='something-wrong', + kid='testkey', ) + + def test_invalid_kid(self): + self.authtoken_raised('Token error: Signature verification failed', kid='doesnotexist') + + +class ExampleOpenIdConnectAuth(OpenIdConnectAuth): + name = 'example123' + OIDC_ENDPOINT = 'https://example.com/oidc' + + +class OpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): + backend_path = \ + 'social_core.tests.backends.test_open_id_connect.ExampleOpenIdConnectAuth' + issuer = 'https://example.com' + openid_config_body = json.dumps({ + 'issuer': 'https://example.com', + 'authorization_endpoint': 'https://example.com/oidc/auth', + 'token_endpoint': 'https://example.com/oidc/token', + 'userinfo_endpoint': 'https://example.com/oidc/userinfo', + 'revocation_endpoint': 'https://example.com/oidc/revoke', + 'jwks_uri': 'https://example.com/oidc/certs', + }) + + expected_username = 'cartman' + + def pre_complete_callback(self, start_url): + super().pre_complete_callback(start_url) + HTTPretty.register_uri('GET', + uri=self.backend.userinfo_url(), + status=200, + body=json.dumps({'preferred_username': self.expected_username}), + content_type='text/json') + + def test_everything_works(self): + self.do_login() diff --git a/social_core/tests/backends/test_saml.py b/social_core/tests/backends/test_saml.py index 85e57949..40a0837f 100644 --- a/social_core/tests/backends/test_saml.py +++ b/social_core/tests/backends/test_saml.py @@ -1,4 +1,3 @@ - import json import os import re @@ -36,7 +35,7 @@ class SAMLTest(BaseBackendTest): def extra_settings(self): name = path.join(DATA_DIR, 'saml_config.json') - with open(name, 'r') as config_file: + with open(name) as config_file: config_str = config_file.read() return json.loads(config_str) @@ -61,7 +60,7 @@ def install_http_intercepts(self, start_url, return_url): # data in the query string. A pre-recorded correct response # is kept in this .txt file: name = path.join(DATA_DIR, 'saml_response.txt') - with open(name, 'r') as response_file: + with open(name) as response_file: response_url = response_file.read() HTTPretty.register_uri(HTTPretty.GET, start_url, status=301, location=response_url) @@ -80,8 +79,8 @@ def do_start(self): response = requests.get(start_url) self.assertTrue(response.url.startswith(return_url)) self.assertEqual(response.text, 'foobar') - query_values = dict((k, v[0]) for k, v in - parse_qs(urlparse(response.url).query).items()) + query_values = {k: v[0] for k, v in + parse_qs(urlparse(response.url).query).items()} self.assertNotIn(' ', query_values['SAMLResponse']) self.strategy.set_request_data(query_values, self.backend) return self.backend.complete() @@ -110,8 +109,8 @@ def modify_start_url(self, start_url): """ # Parse the SAML Request URL to get the XML being sent to TestShib url_parts = urlparse(start_url) - query = dict((k, v[0]) for (k, v) in - parse_qs(url_parts.query).items()) + query = {k: v[0] for (k, v) in + parse_qs(url_parts.query).items()} xml = OneLogin_Saml2_Utils.decode_base64_and_inflate( query['SAMLRequest'] ) diff --git a/social_core/tests/backends/test_slack.py b/social_core/tests/backends/test_slack.py index 7055f574..f73f8e5c 100644 --- a/social_core/tests/backends/test_slack.py +++ b/social_core/tests/backends/test_slack.py @@ -15,12 +15,12 @@ class SlackOAuth2Test(OAuth2Test): 'user': { 'email': 'foobar@example.com', 'name': 'Foo Bar', - 'id': u'123456' + 'id': '123456' }, 'team': { - 'id': u'456789' + 'id': '456789' }, - 'scope': u'identity.basic,identity.email' + 'scope': 'identity.basic,identity.email' }) expected_username = 'foobar' @@ -38,13 +38,13 @@ class SlackOAuth2TestTeamName(SlackOAuth2Test): 'user': { 'email': 'foobar@example.com', 'name': 'Foo Bar', - 'id': u'123456' + 'id': '123456' }, 'team': { - 'id': u'456789', - 'name': u'Square', + 'id': '456789', + 'name': 'Square', }, - 'scope': u'identity.basic,identity.email,identity.team' + 'scope': 'identity.basic,identity.email,identity.team' }) @@ -54,13 +54,13 @@ class SlackOAuth2TestUnicodeTeamName(SlackOAuth2Test): 'user': { 'email': 'foobar@example.com', 'name': 'Foo Bar', - 'id': u'123456' + 'id': '123456' }, 'team': { - 'id': u'456789', - 'name': u'Square \u221a team', + 'id': '456789', + 'name': 'Square \u221a team', }, - 'scope': u'identity.basic,identity.email,identity.team' + 'scope': 'identity.basic,identity.email,identity.team' }) def test_login(self): diff --git a/social_core/tests/backends/test_twitch.py b/social_core/tests/backends/test_twitch.py index 67b1e375..2907f809 100644 --- a/social_core/tests/backends/test_twitch.py +++ b/social_core/tests/backends/test_twitch.py @@ -1,32 +1,80 @@ import json from .oauth import OAuth2Test +from .test_open_id_connect import OpenIdConnectTestMixin + + +class TwitchOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): + backend_path = 'social_core.backends.twitch.TwitchOpenIdConnect' + user_data_url = 'https://id.twitch.tv/oauth2/userinfo' + issuer = 'https://id.twitch.tv/oauth2' + expected_username = 'test_user1' + openid_config_body = json.dumps({ + 'authorization_endpoint': 'https://id.twitch.tv/oauth2/authorize', + 'claims_parameter_supported': True, + 'claims_supported': [ + 'iss', + 'azp', + 'preferred_username', + 'updated_at', + 'aud', + 'exp', + 'iat', + 'picture', + 'sub', + 'email', + 'email_verified', + ], + 'id_token_signing_alg_values_supported': [ + 'RS256', + ], + 'issuer': 'https://id.twitch.tv/oauth2', + 'jwks_uri': 'https://id.twitch.tv/oauth2/keys', + 'response_types_supported': [ + 'id_token', + 'code', + 'token', + 'code id_token', + 'token id_token', + ], + 'scopes_supported': [ + 'openid', + ], + 'subject_types_supported': [ + 'public', + ], + 'token_endpoint': 'https://id.twitch.tv/oauth2/token', + 'token_endpoint_auth_methods_supported': [ + 'client_secret_post', + ], + 'userinfo_endpoint': 'https://id.twitch.tv/oauth2/userinfo', + }) class TwitchOAuth2Test(OAuth2Test): backend_path = 'social_core.backends.twitch.TwitchOAuth2' - user_data_url = 'https://api.twitch.tv/kraken/user/' + user_data_url = 'https://api.twitch.tv/helix/users' expected_username = 'test_user1' access_token_body = json.dumps({ 'access_token': 'foobar', + 'token_type': 'bearer', }) user_data_body = json.dumps({ - 'type': 'user', - 'name': 'test_user1', - 'created_at': '2011-06-03T17:49:19Z', - 'updated_at': '2012-06-18T17:19:57Z', - 'logo': 'http://static-cdn.jtvnw.net/jtv_user_pictures/' - 'test_user1-profile_image-62e8318af864d6d7-300x300.jpeg', - '_id': 22761313, - 'display_name': 'test_user1', - 'bio': 'test bio woo I\'m a test user', - 'email': 'asdf@asdf.com', - 'email_verified': True, - 'partnered': True, - 'twitter_connected': False, - 'notifications': { - 'push': True, - 'email': True - } + 'data': [ + { + 'id': '689563726', + 'login': 'test_user1', + 'display_name': 'test_user1', + 'type': '', + 'broadcaster_type': '', + 'description': '', + 'profile_image_url': 'https://static-cdn.jtvnw.net/jtv_user_pictures/foo.png', + 'offline_image_url': '', + 'view_count': 0, + 'email': 'example@reply.com', + 'created_at': '2021-05-21T18:59:25Z', + 'access_token': 'hmkgz15x7j54jm63rpwfwhcnue6t4fxwv' + } + ] }) def test_login(self): diff --git a/social_core/tests/backends/test_vk.py b/social_core/tests/backends/test_vk.py index 606b899d..6d8c2279 100644 --- a/social_core/tests/backends/test_vk.py +++ b/social_core/tests/backends/test_vk.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - import json from .oauth import OAuth2Test diff --git a/social_core/tests/test_partial.py b/social_core/tests/test_partial.py index b5ae3c49..67439c73 100644 --- a/social_core/tests/test_partial.py +++ b/social_core/tests/test_partial.py @@ -17,8 +17,8 @@ def setUp(self): self.mock_partial_store = Mock() self.mock_strategy.storage.partial.store = self.mock_partial_store - self.mock_sesstion_set = Mock() - self.mock_strategy.session_set = self.mock_sesstion_set + self.mock_session_set = Mock() + self.mock_strategy.session_set = self.mock_session_set def test_save_to_session(self): # GIVEN @@ -42,10 +42,10 @@ def decorated_func(*args, **kwargs): self.assertEqual((self.mock_current_partial,), self.mock_partial_store.call_args[0]) - self.assertEqual(1, self.mock_sesstion_set.call_count) + self.assertEqual(1, self.mock_session_set.call_count) self.assertEqual((PARTIAL_TOKEN_SESSION_NAME, self.mock_current_partial_token), - self.mock_sesstion_set.call_args[0]) + self.mock_session_set.call_args[0]) def test_not_to_save_to_session(self): # GIVEN @@ -69,7 +69,7 @@ def decorated_func(*args, **kwargs): self.assertEqual((self.mock_current_partial,), self.mock_partial_store.call_args[0]) - self.assertEqual(0, self.mock_sesstion_set.call_count) + self.assertEqual(0, self.mock_session_set.call_count) def test_save_to_session_by_backward_compatible_decorator(self): # GIVEN @@ -93,10 +93,10 @@ def decorated_func(*args, **kwargs): self.assertEqual((self.mock_current_partial,), self.mock_partial_store.call_args[0]) - self.assertEqual(1, self.mock_sesstion_set.call_count) + self.assertEqual(1, self.mock_session_set.call_count) self.assertEqual((PARTIAL_TOKEN_SESSION_NAME, self.mock_current_partial_token), - self.mock_sesstion_set.call_args[0]) + self.mock_session_set.call_args[0]) def test_not_to_save_to_session_when_the_response_is_a_dict(self): # GIVEN @@ -114,4 +114,4 @@ def decorated_func(*args, **kwargs): # THEN self.assertEqual(expected_response, response) self.assertEqual(0, self.mock_partial_store.call_count) - self.assertEqual(0, self.mock_sesstion_set.call_count) + self.assertEqual(0, self.mock_session_set.call_count) diff --git a/social_core/tests/test_utils.py b/social_core/tests/test_utils.py index 19543f19..a54b296d 100644 --- a/social_core/tests/test_utils.py +++ b/social_core/tests/test_utils.py @@ -45,7 +45,7 @@ def test_valid_relative_redirect(self): def test_multiple_hosts(self): allowed_hosts = ['myapp1.com', 'myapp2.com'] for host in allowed_hosts: - url = 'http://{}/path/'.format(host) + url = f'http://{host}/path/' self.assertEqual(sanitize_redirect(allowed_hosts, url), url) def test_multiple_hosts_wrong_host(self): diff --git a/social_core/utils.py b/social_core/utils.py index 2e2523c3..1970ef7f 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -265,7 +265,7 @@ def append_slash(url): 'http://www.example.com/api/user/1/' """ if url and not url.endswith('/'): - url = '{0}/'.format(url) + url = f'{url}/' return url @@ -305,4 +305,9 @@ def wrapped(this): if not cached_value: raise return cached_value + + wrapped.invalidate = self._invalidate return wrapped + + def _invalidate(self): + self.cache.clear()