From: Stas Kravets Date: Mon, 7 May 2012 09:11:07 +0000 (+0400) Subject: Merging of vkontakte and yandex backends with bacher09 versions. Preparations for... X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=a8a1539d67a461beafaf49c0e68e61ecb168e59a;p=django-social-auth.git Merging of vkontakte and yandex backends with bacher09 versions. Preparations for merge with omab master. --- diff --git a/example/local_settings.py.template b/example/local_settings.py.template index bcb8157..827b9e0 100644 --- a/example/local_settings.py.template +++ b/example/local_settings.py.template @@ -36,8 +36,13 @@ FOURSQUARE_CONSUMER_SECRET = '' YANDEX_OAUTH2_CLIENT_KEY = '' YANDEX_OAUTH2_CLIENT_SECRET = '' YANDEX_OAUTH2_API_URL = 'https://api-yaru.yandex.ru/me/' # http://api.moikrug.ru/v1/my/ for Moi Krug -VK_APP_ID = '' -VK_API_SECRET = '' + +# Backward compatibility +YANDEX_APP_ID = YANDEX_OAUTH2_CLIENT_KEY +YANDEX_API_SECRET = YANDEX_OAUTH2_CLIENT_SECRET + +VK_APP_ID = VKONTAKTE_APP_ID +VK_API_SECRET = VKONTAKTE_APP_SECRET SOCIAL_AUTH_PIPELINE = ( 'social_auth.backends.pipeline.social.social_auth_user', diff --git a/example/settings.py b/example/settings.py index 351c033..bbf2113 100644 --- a/example/settings.py +++ b/example/settings.py @@ -84,8 +84,8 @@ AUTHENTICATION_BACKENDS = ( 'social_auth.backends.contrib.livejournal.LiveJournalBackend', 'social_auth.backends.browserid.BrowserIDBackend', 'social_auth.backends.contrib.vkontakte.VKontakteBackend', - 'social_auth.backends.contrib.yandex.YandexBackend', 'social_auth.backends.contrib.yandex.YandexOAuth2Backend', + 'social_auth.backends.contrib.yandex.YaruBackend', 'social_auth.backends.contrib.odnoklassniki.OdnoklassnikiBackend', 'social_auth.backends.contrib.vkontakte.VKontakteOAuth2Backend', 'social_auth.backends.contrib.mailru.MailruBackend', diff --git a/social_auth/backends/contrib/vkontakte.py b/social_auth/backends/contrib/vkontakte.py index ea83884..c23747c 100644 --- a/social_auth/backends/contrib/vkontakte.py +++ b/social_auth/backends/contrib/vkontakte.py @@ -8,30 +8,32 @@ www.vkontakte.ru. Username is retrieved from the identity returned by server. import logging logger = logging.getLogger(__name__) -from django.conf import settings from django.contrib.auth import authenticate from django.utils import simplejson -from urllib import urlencode, unquote -from urllib2 import Request, urlopen, HTTPError +from urllib import urlencode +from urllib2 import urlopen from hashlib import md5 from time import time from social_auth.backends import SocialAuthBackend, OAuthBackend, BaseAuth, BaseOAuth2, USERNAME from social_auth.utils import setting +# Vkontakte configuration +VK_AUTHORIZATION_URL = 'http://oauth.vk.com/authorize' +VK_ACCESS_TOKEN_URL = 'https://oauth.vk.com/access_token' +VK_SERVER = 'vk.com' +VK_DEFAULT_DATA = ['first_name','last_name','screen_name','nickname', 'photo'] + VKONTAKTE_API_URL = 'https://api.vkontakte.ru/method/' VKONTAKTE_SERVER_API_URL = 'http://api.vkontakte.ru/api.php' VKONTAKTE_API_VERSION = '3.0' -VKONTAKTE_OAUTH2_SCOPE = [''] # Enough for authentication - -EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires') -USE_APP_AUTH = getattr(settings, 'VKONTAKTE_APP_AUTH', False) -LOCAL_HTML = getattr(settings, 'VKONTAKTE_LOCAL_HTML', 'vkontakte.html') +USE_APP_AUTH = setting('VKONTAKTE_APP_AUTH', False) +LOCAL_HTML = setting('VKONTAKTE_LOCAL_HTML', 'vkontakte.html') class VKontakteBackend(SocialAuthBackend): - """VKontakte authentication backend""" + """VKontakte OpenAPI authentication backend""" name = 'vkontakte' def get_user_id(self, details, response): @@ -40,53 +42,19 @@ class VKontakteBackend(SocialAuthBackend): def get_user_details(self, response): """Return user details from VKontakte request""" - nickname = unquote(response.GET['nickname']) + nickname = response.GET['nickname'] values = { USERNAME: response.GET['id'] if len(nickname) == 0 else nickname, 'email': '', 'fullname': '', - 'first_name': unquote(response.GET['first_name']), 'last_name': unquote(response.GET['last_name'])} - return values - - -class VKontakteOAuth2Backend(OAuthBackend): - """VKontakteOAuth2 authentication backend""" - name = 'vkontakte-oauth2' - EXTRA_DATA = [('expires_in', EXPIRES_NAME)] - - def get_user_id(self, details, response): - """Return user unique id provided by VKontakte""" - return int(response['user_id']) - - def get_user_details(self, response): - """Return user details from VKontakte request""" - values = { USERNAME: str(response['user_id']), 'email': ''} - - details = response['response'] - user_name = details.get('user_name') - - if user_name: - values['fullname'] = unquote(user_name) - - if ' ' in values['fullname']: - values['first_name'], values['last_name'] = values['fullname'].split() - else: - values['first_name'] = values['fullname'] - - if 'last_name' in details: - values['last_name'] = unquote(details['last_name']) - - if 'first_name' in details: - values['first_name'] = unquote(details['first_name']) - + 'first_name': response.GET['first_name'], 'last_name': response.GET['last_name']} return values class VKontakteAuth(BaseAuth): """VKontakte OpenAPI authorization mechanism""" AUTH_BACKEND = VKontakteBackend - APP_ID = settings.VKONTAKTE_APP_ID + APP_ID = setting('VKONTAKTE_APP_ID') def auth_html(self): """Returns local VK authentication page, not necessary for VK to authenticate """ - from django.core.urlresolvers import reverse from django.template import RequestContext, loader dict = { 'VK_APP_ID' : self.APP_ID, @@ -107,7 +75,7 @@ class VKontakteAuth(BaseAuth): cookie_dict = dict(item.split('=') for item in self.request.COOKIES[app_cookie].split('&')) check_str = ''.join([item + '=' + cookie_dict[item] for item in ['expire', 'mid', 'secret', 'sid']]) - hash = md5(check_str + settings.VKONTAKTE_APP_SECRET).hexdigest() + hash = md5(check_str + setting('VKONTAKTE_APP_SECRET')).hexdigest() if hash != cookie_dict['sig'] or int(cookie_dict['expire']) < time() : raise ValueError('VKontakte authentication failed: invalid hash') @@ -123,16 +91,59 @@ class VKontakteAuth(BaseAuth): return False +class VKontakteOAuth2Backend(OAuthBackend): + """VKontakteOAuth2 authentication backend""" + name = 'vkontakte-oauth2' + + EXTRA_DATA = [ + ('id', 'id'), + ('expires', setting('SOCIAL_AUTH_EXPIRATION', 'expires')) + ] + + def get_user_id(self, details, response): + """OAuth providers return an unique user id in response""" + return response['user_id'] + + def get_user_details(self, response): + """Return user details from Vkontakte account""" + return {USERNAME: response.get('screen_name'), + 'email': '', + 'first_name': response.get('first_name'), + 'last_name': response.get('last_name')} + + class VKontakteOAuth2(BaseOAuth2): - """VKontakte OAuth2 support""" + """Vkontakte OAuth mechanism""" + AUTHORIZATION_URL = VK_AUTHORIZATION_URL + ACCESS_TOKEN_URL = VK_ACCESS_TOKEN_URL + SERVER_URL = VK_SERVER AUTH_BACKEND = VKontakteOAuth2Backend - AUTHORIZATION_URL = 'http://api.vkontakte.ru/oauth/authorize' - ACCESS_TOKEN_URL = ' https://api.vkontakte.ru/oauth/access_token' - SETTINGS_KEY_NAME = 'VKONTAKTE_APP_ID' - SETTINGS_SECRET_NAME = 'VKONTAKTE_APP_SECRET' + SETTINGS_KEY_NAME = 'VK_APP_ID' + SETTINGS_SECRET_NAME = 'VK_API_SECRET' + # Look at http://vk.com/developers.php?oid=-1&p=%D0%9F%D1%80%D0%B0%D0%B2%D0%B0_%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%D0%B0_%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B9 + SCOPE_VAR_NAME = 'VK_EXTRA_SCOPE' def get_scope(self): - return setting('VKONTAKTE_OAUTH2_EXTRA_SCOPE', []) + return setting(VKontakteOAuth2.SCOPE_VAR_NAME) or setting('VKONTAKTE_OAUTH2_EXTRA_SCOPE') + + def user_data(self, access_token, response, *args, **kwargs): + """Loads user data from service""" + fields = ','.join(VK_DEFAULT_DATA + setting('VK_EXTRA_DATA',[])) + params = {'access_token': access_token, + 'fields': fields, + 'uids': response.get('user_id')} + + data = vkontakte_api('users.get', params) + + if data: + data = data.get('response')[0] + data['user_photo'] = data.get('photo') # Backward compatibility + + return data + + +class VKontakteAppAuth(VKontakteOAuth2): + """VKontakte Application Authentication support""" def auth_complete(self, *args, **kwargs): if USE_APP_AUTH: @@ -144,18 +155,7 @@ class VKontakteOAuth2(BaseOAuth2): if stop: return None - try: - auth_result = super(VKontakteOAuth2, self).auth_complete(*args, **kwargs) - except HTTPError: # VKontakte returns HTTPError 400 if cancelled - raise ValueError('Authentication cancelled') - - return auth_result - - def user_data(self, access_token, *args, **kwargs): - """Return user data from VKontakte API""" - data = {'access_token': access_token } - - return vkontakte_api('getUserInfoEx', data) + return super(VKontakteAppAuth, self).auth_complete(*args, **kwargs) def user_profile(self, user_id, access_token = None): data = {'uids': user_id, 'fields': 'photo'} @@ -188,8 +188,10 @@ class VKontakteOAuth2(BaseOAuth2): # Verify signature, if present if auth_key: - check_key = md5(self.request.REQUEST.get('api_id') + '_' + self.request.REQUEST.get('viewer_id') + '_' + \ - USE_APP_AUTH['key']).hexdigest() + check_key = md5('_'.join([self.request.REQUEST.get('api_id'), + self.request.REQUEST.get('viewer_id'), + USE_APP_AUTH['key']])).hexdigest() + if check_key != auth_key: raise ValueError('VKontakte authentication failed: invalid auth key') @@ -207,6 +209,13 @@ class VKontakteOAuth2(BaseOAuth2): return (True, authenticate(**{'response': data, self.AUTH_BACKEND.name: True})) +def _api_get_val_fun(name, conf): + if USE_APP_AUTH: + return USE_APP_AUTH.get(name) + else: + return setting(conf) + + def vkontakte_api(method, data): """ Calls VKontakte OpenAPI method http://vkontakte.ru/apiclub, @@ -219,13 +228,13 @@ def vkontakte_api(method, data): data['v'] = VKONTAKTE_API_VERSION if not 'api_id' in data: - data['api_id'] = USE_APP_AUTH.get('id') if USE_APP_AUTH else settings.VKONTAKTE_APP_ID + data['api_id'] = _api_get_val_fun('id','VKONTAKTE_APP_ID') data['method'] = method data['format'] = 'json' url = VKONTAKTE_SERVER_API_URL - secret = USE_APP_AUTH.get('key') if USE_APP_AUTH else settings.VKONTAKTE_APP_SECRET + secret = _api_get_val_fun('key','VKONTAKTE_APP_SECRET') param_list = sorted(list(item + '=' + data[item] for item in data)) data['sig'] = md5(''.join(param_list) + secret).hexdigest() @@ -233,14 +242,13 @@ def vkontakte_api(method, data): url = VKONTAKTE_API_URL + method params = urlencode(data) - api_request = Request(url + '?' + params) + url += '?' + params try: - return simplejson.loads(urlopen(api_request).read()) + return simplejson.load(urlopen(url)) except (TypeError, KeyError, IOError, ValueError, IndexError): - logger.error('Could not load data from VKontakte.', exc_info=True, extra=dict(data=params)) + logger.error('Could not load data from VKontakte.', exc_info=True, extra=dict(data=data)) return None - # Backend definition BACKENDS = { 'vkontakte': VKontakteAuth, diff --git a/social_auth/backends/contrib/yandex.py b/social_auth/backends/contrib/yandex.py index 6cffc9d..edd6477 100644 --- a/social_auth/backends/contrib/yandex.py +++ b/social_auth/backends/contrib/yandex.py @@ -1,5 +1,5 @@ """ -Yandex OpenID support. +Yandex OpenID and OAuth2 support. This contribution adds support for Yandex.ru OpenID service in the form openid.yandex.ru/user. Username is retrieved from the identity url. @@ -7,65 +7,50 @@ openid.yandex.ru/user. Username is retrieved from the identity url. If username is not specified, OpenID 2.0 url used for authentication. """ import logging -logger = logging.getLogger(__name__) +from django.utils import simplejson -import urlparse +logger = logging.getLogger(__name__) from urllib import urlencode, unquote -from urllib2 import Request, urlopen, HTTPError +from urllib2 import urlopen +from urlparse import urlparse, urlsplit + +from social_auth.backends import OpenIDBackend, OpenIdAuth, USERNAME,\ + OAuthBackend, BaseOAuth2 -from django.conf import settings -import xml.dom.minidom +from social_auth.utils import setting -from social_auth.backends import OpenIDBackend, OpenIdAuth, USERNAME, OAuthBackend, BaseOAuth2 +# Yandex configuration +YANDEX_AUTHORIZATION_URL = 'https://oauth.yandex.ru/authorize' +YANDEX_ACCESS_TOKEN_URL = 'https://oauth.yandex.ru/token' +YANDEX_SERVER = 'oauth.yandex.ru' +YANDEX_OPENID_URL = 'http://openid.yandex.ru' -# Yandex conf -YANDEX_URL = 'http://openid.yandex.ru/%s' -YANDEX_USER_FIELD = 'openid_ya_user' -YANDEX_OID_2_URL = 'http://yandex.ru' -EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires') +def get_username_from_url(links): + try: + host = urlparse(links.get('www')).hostname + return host.split('.')[0] + except (IndexError, AttributeError): + return None + class YandexBackend(OpenIDBackend): """Yandex OpenID authentication backend""" name = 'yandex' + def get_user_id(self, details, response): + return details['email'] or response.identity_url + def get_user_details(self, response): """Generate username from identity url""" values = super(YandexBackend, self).get_user_details(response) values[USERNAME] = values.get(USERNAME) or\ - urlparse.urlsplit(response.identity_url)\ + urlsplit(response.identity_url)\ .path.strip('/') - values['email'] = values.get('email') or '' - - return values - - -class YandexOAuth2Backend(OAuthBackend): - """Yandex OAuth2 authentication backend""" - name = 'yandex-oauth2' - - def get_user_id(self, details, response): - """Return user unique id provided by Yandex""" - return int(response['id']) - - def get_user_details(self, response): - """Return user details from Yandex request""" - - name = unquote(response['name']) - first_name = '' - last_name = '' - - if ' ' in name: - last_name, first_name = name.split(' ') - name = first_name - else: - first_name = name - - values = { USERNAME: name, 'email': '', - 'first_name': first_name, 'last_name': last_name} + values['email'] = values.get('email', '') return values @@ -76,89 +61,93 @@ class YandexAuth(OpenIdAuth): def openid_url(self): """Returns Yandex authentication URL""" - if YANDEX_USER_FIELD not in self.data: - return YANDEX_OID_2_URL - else: - return YANDEX_URL % self.data[YANDEX_USER_FIELD] - - -class YandexOAuth2(BaseOAuth2): - """Yandex OAuth2 support - See http://api.yandex.ru/oauth/doc/dg/concepts/About.xml for details""" - AUTH_BACKEND = YandexOAuth2Backend - AUTHORIZATION_URL = 'https://oauth.yandex.ru/authorize' - ACCESS_TOKEN_URL = 'https://oauth.yandex.ru/token' - SETTINGS_KEY_NAME = 'YANDEX_OAUTH2_CLIENT_KEY' - SETTINGS_SECRET_NAME = 'YANDEX_OAUTH2_CLIENT_SECRET' + return YANDEX_OPENID_URL - def get_scope(self): - return [] # Yandex does not allow custom scope - def auth_complete(self, *args, **kwargs): - try: - auth_result = super(YandexOAuth2, self).auth_complete(*args, **kwargs) - except HTTPError: # Returns HTTPError 400 if cancelled - raise ValueError('Authentication cancelled') +class YaruBackend(OAuthBackend): + """Yandex OAuth authentication backend""" + name = 'yaru' + EXTRA_DATA = [ + ('id', 'id'), + ('expires', setting('SOCIAL_AUTH_EXPIRATION', 'expires')) + ] - return auth_result + def get_user_details(self, response): + name = response['name'] + last_name = '' - def user_data(self, access_token, *args, **kwargs): - """Return user data from Yandex REST API specified in settings""" - params = urlencode({'text': 1, 'format': 'xml'}) - request = Request(settings.YANDEX_OAUTH2_API_URL + '?' + params, headers={'Authorization': "OAuth " + access_token }) + if ' ' in name: + names = name.split(' ') + last_name = names[0] + first_name = names[1] + else: + first_name = name + """Return user details from Yandex account""" + return { USERNAME: get_username_from_url(response.get('links')), + 'email': response.get('email', ''), + 'first_name': first_name, 'last_name': last_name, + } + + +class YaruAuth(BaseOAuth2): + """Yandex Ya.ru OAuth mechanism""" + AUTHORIZATION_URL = YANDEX_AUTHORIZATION_URL + ACCESS_TOKEN_URL = YANDEX_ACCESS_TOKEN_URL + AUTH_BACKEND = YaruBackend + SERVER_URL = YANDEX_SERVER + SETTINGS_KEY_NAME = 'YANDEX_APP_ID' + SETTINGS_SECRET_NAME = 'YANDEX_API_SECRET' + + def get_api_url(self): + return 'https://api-yaru.yandex.ru/me/' + + def user_data(self, access_token, response, *args, **kwargs): + """Loads user data from service""" + params = {'oauth_token': access_token, + 'format': 'json', + 'text': 1, + } + + url = self.get_api_url() + '?' + urlencode(params) try: - reply = urlopen(request).read() - dom = xml.dom.minidom.parseString(reply) - - id = getNodeText(dom, "id") - if "/" in id: - id = id.split("/")[-1] - - name = getNodeText(dom, "name") - - links = getNodesWithAttribute(dom, "link", {"rel": "userpic"}) - if not links: - userpic = getNodeText(dom, "Portrait") - else: - userpic = links[0].getAttribute("href") if links else "" - - return {"id": id, "name": name, "userpic": userpic, "access_token": access_token} - except (TypeError, KeyError, IOError, ValueError, IndexError): + return simplejson.load(urlopen(url)) + except (ValueError, IndexError): logger.error('Could not load data from Yandex.', exc_info=True, extra=dict(data=params)) return None -def getNodeText(dom, nodeName): - node = dom.getElementsByTagName(nodeName) +class YandexOAuth2Backend(YaruBackend): + """Legacy Yandex OAuth2 authentication backend""" + name = 'yandex-oauth2' + + +class YandexOAuth2(YaruAuth): + """Yandex Ya.ru/Moi Krug OAuth mechanism""" + AUTH_BACKEND = YandexOAuth2Backend - if node: - nodelist = node[0].childNodes - else: - return '' + def get_api_url(self): + return setting('YANDEX_OAUTH2_API_URL') - rc = [] - for node in nodelist: - if node.nodeType == node.TEXT_NODE: - rc.append(node.data) + def user_data(self, access_token, response, *args, **kwargs): + reply = super(YandexOAuth2, self).user_data(access_token, response, args, kwargs) - return ''.join(rc) + if reply: + if isinstance(reply, list) and len(reply) >= 1: + reply = reply[0] -def getNodesWithAttribute(dom, nodeName, attrDict): - nodelist = dom.getElementsByTagName(nodeName) - found = [] + if 'links' in reply: + userpic = reply['links'].get('avatar') + elif 'avatar' in reply: + userpic = reply['avatar'].get('Portrait') - for node in nodelist: - for key, value in attrDict.items(): - if node.hasAttribute(key): - if value and node.getAttribute(key) != value: - continue - found.append(node) + reply.update({"id":reply["id"].split("/")[-1], "access_token": access_token, "userpic": userpic or ''}) - return found + return reply # Backend definition BACKENDS = { 'yandex': YandexAuth, + 'yaru': YaruAuth, 'yandex-oauth2': YandexOAuth2 }