diff --git a/CHANGELOG.md b/CHANGELOG.md index 20d10567f..a16bcfac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Updated list of default user protected fields to include admin flags and password +- Instagram: Update to use the latest Graph API v3.2 ## [3.3.2](https://github.com/python-social-auth/social-core/releases/tag/3.3.2) - 2020-03-25 diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index 475761c87..f146c17cc 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -149,8 +149,8 @@ def do_auth(self, access_token, response=None, *args, **kwargs): if self.data.get('denied_scopes'): data['denied_scopes'] = self.data['denied_scopes'].split(',') - kwargs.update({'backend': self, 'response': data}) - return self.strategy.authenticate(*args, **kwargs) + kwargs.update({'response': data}) + return self.strategy.authenticate(self,*args, **kwargs) def revoke_token_url(self, token, uid): version = self.setting('API_VERSION', API_VERSION) diff --git a/social_core/backends/instagram.py b/social_core/backends/instagram.py index cc138e576..bfa1c804e 100644 --- a/social_core/backends/instagram.py +++ b/social_core/backends/instagram.py @@ -3,51 +3,83 @@ https://python-social-auth.readthedocs.io/en/latest/backends/instagram.html """ import hmac +import json from hashlib import sha256 from .oauth import BaseOAuth2 +from ..utils import handle_http_errors, parse_qs +from ..exceptions import AuthCanceled, AuthMissingParameter class InstagramOAuth2(BaseOAuth2): name = 'instagram' + REDIRECT_STATE = False + SCOPE_SEPARATOR = ',' AUTHORIZATION_URL = 'https://api.instagram.com/oauth/authorize' ACCESS_TOKEN_URL = 'https://api.instagram.com/oauth/access_token' + USER_DATA_URL = 'https://graph.instagram.com/me/' + REFRESH_TOKEN_URL = 'https://graph.instagram.com/refresh_access_token' ACCESS_TOKEN_METHOD = 'POST' - def get_user_id(self, details, response): - # Sometimes Instagram returns 'user', sometimes 'data', but API docs - # says 'data' http://instagram.com/developer/endpoints/users/#get_users - user = response.get('user') or response.get('data') or {} - return user.get('id') def get_user_details(self, response): """Return user details from Instagram account""" - # Sometimes Instagram returns 'user', sometimes 'data', but API docs - # says 'data' http://instagram.com/developer/endpoints/users/#get_users - user = response.get('user') or response.get('data') or {} - username = user['username'] - email = user.get('email', '') + # https://developers.facebook.com/docs/instagram-basic-display-api/reference/me fullname, first_name, last_name = self.get_user_names( - user.get('full_name', '') + response.get('full_name', '') ) - return {'username': username, + return {'username': response.get('username', response.get('name')), + 'email': response.get('email', ''), 'fullname': fullname, 'first_name': first_name, - 'last_name': last_name, - 'email': email} + 'last_name': last_name} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - key, secret = self.get_key_and_secret() - params = {'access_token': access_token} - sig = self._generate_sig("/users/self", params, secret) - params['sig'] = sig - return self.get_json('https://api.instagram.com/v1/users/self', + params = self.setting('PROFILE_EXTRA_PARAMS', {}) + params['access_token'] = access_token + return self.get_json(self.USER_DATA_URL, params=params) - def _generate_sig(self, endpoint, params, secret): - sig = endpoint - for key in sorted(params.keys()): - sig += '|%s=%s' % (key, params[key]) - return hmac.new(secret.encode(), sig.encode(), sha256).hexdigest() + def process_error(self, data): + super(InstagramOAuth2, self).process_error(data) + if data.get('error_code'): + raise AuthCanceled(self, data.get('error_message') or + data.get('error_code')) + + @handle_http_errors + def auth_complete(self, *args, **kwargs): + """Completes login process, must return user instance""" + self.process_error(self.data) + if not self.data.get('code'): + raise AuthMissingParameter(self, 'code') + state = self.validate_state() + key, secret = self.get_key_and_secret() + response = self.request( + self.ACCESS_TOKEN_URL, + data={ + 'client_id': key, + 'client_secret': secret, + 'code': self.data['code'], + 'grant_type': 'authorization_code', + 'redirect_uri': self.get_redirect_uri(state), + }, + method=self.ACCESS_TOKEN_METHOD + ) + # API v2.3 returns a JSON, according to the documents linked at issue + # #592, but it seems that this needs to be enabled(?), otherwise the + # usual querystring type response is returned. + try: + response = response.json() + except ValueError: + response = parse_qs(response.text) + access_token = response['access_token'] + return self.do_auth(access_token, response=response, *args, **kwargs) + + + def refresh_token_params(self, token, *args, **kwargs): + return { + 'access_token': token, + 'grant_type': 'ig_refresh_token', + }