From: Stas Kravets Date: Wed, 2 May 2012 09:35:25 +0000 (+0400) Subject: Merge remote-tracking branch 'upstream/master' X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=29e1436d16d609cbe6f440aa75363e9d25d7bfbf;p=django-social-auth.git Merge remote-tracking branch 'upstream/master' Conflicts: README.rst example/local_settings.py.template example/templates/done.html social_auth/backends/contrib/vkontakte.py social_auth/backends/contrib/yandex.py social_auth/backends/facebook.py social_auth/views.py --- 29e1436d16d609cbe6f440aa75363e9d25d7bfbf diff --cc README.rst index 0ceeb32,188810d..5ba9021 --- a/README.rst +++ b/README.rst @@@ -967,11 -1092,27 +1095,26 @@@ Mailing lis Join to `django-social-auth discussion list`_ and bring any questions or suggestions that would improve this application. Convore_ discussion group is deprecated since the service is going to be shut down on April 1st. - If defining a custom user model, do not import social_auth from any models.py - that would finally import from the models.py that defines your User class or it - will make your project fail with a recursive import because social_auth uses - get_model() to retrieve your User. -- + South users + ^^^^^^^^^^^ + South_ users should add this rule to enable migrations:: + + try: + import south + from south.modelsinspector import add_introspection_rules + add_introspection_rules([], ["^social_auth\.fields\.JSONField"]) + except: + pass + + Custom User model + ^^^^^^^^^^^^^^^^^ + If defining a custom user model, do not import ``social_auth`` from any + ``models.py`` that would finally import from the ``models.py`` that defines + your ``User`` class or it will make your project fail with a recursive import + because ``social_auth`` uses ``get_model()`` to retrieve your User. + + Third party backends + ^^^^^^^^^^^^^^^^^^^^ There's an ongoing movement to create a list of third party backends on djangopackages.com_, so, if somebody doesn't want it's backend in the ``contrib`` directory but still wants to share, just split it in a separated diff --cc example/local_settings.py.template index bfbb541,09585d5..bcb8157 --- a/example/local_settings.py.template +++ b/example/local_settings.py.template @@@ -31,9 -21,8 +33,11 @@@ GITHUB_APP_ID = ' GITHUB_API_SECRET = '' FOURSQUARE_CONSUMER_KEY = '' 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 = '' SOCIAL_AUTH_PIPELINE = ( 'social_auth.backends.pipeline.social.social_auth_user', diff --cc example/settings.py index a6a26ac,a9d30b1..351c033 --- a/example/settings.py +++ b/example/settings.py @@@ -75,8 -75,13 +75,11 @@@ AUTHENTICATION_BACKENDS = 'social_auth.backends.google.GoogleBackend', 'social_auth.backends.yahoo.YahooBackend', 'social_auth.backends.contrib.linkedin.LinkedinBackend', + 'social_auth.backends.contrib.skyrock.SkyrockBackend', 'social_auth.backends.contrib.flickr.FlickrBackend', 'social_auth.backends.contrib.instagram.InstagramBackend', + 'social_auth.backends.contrib.github.GithubBackend', - 'social_auth.backends.contrib.vkontakte.VkontakteBackend', + 'social_auth.backends.contrib.yandex.YandexBackend', - 'social_auth.backends.contrib.yandex.YaruBackend', 'social_auth.backends.OpenIDBackend', 'social_auth.backends.contrib.livejournal.LiveJournalBackend', 'social_auth.backends.browserid.BrowserIDBackend', diff --cc social_auth/backends/__init__.py index 0080361,0f71154..73c8fad --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@@ -17,8 -17,8 +17,8 @@@ from openid.consumer.consumer import Co from openid.consumer.discover import DiscoveryFailure from openid.extensions import sreg, ax --from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest, \ -- SignatureMethod_HMAC_SHA1 ++from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest,\ ++ SignatureMethod_HMAC_SHA1 from django.db import models from django.contrib.auth import authenticate @@@ -26,13 -26,13 +26,13 @@@ from django.contrib.auth.backends impor from django.utils import simplejson from django.utils.importlib import import_module --from social_auth.utils import setting, log, model_to_ctype, ctype_to_model, \ -- clean_partial_pipeline ++from social_auth.utils import setting, log, model_to_ctype, ctype_to_model,\ ++ clean_partial_pipeline from social_auth.store import DjangoOpenIDStore --from social_auth.backends.exceptions import StopPipeline, AuthException, \ -- AuthFailed, AuthCanceled, \ -- AuthUnknownError, AuthTokenError, \ -- AuthMissingParameter ++from social_auth.backends.exceptions import StopPipeline, AuthException,\ ++ AuthFailed, AuthCanceled,\ ++ AuthUnknownError, AuthTokenError,\ ++ AuthMissingParameter if setting('SOCIAL_AUTH_USER_MODEL'): @@@ -55,7 -55,7 +55,7 @@@ AX_SCHEMA_ATTRS = ('http://axschema.org/namePerson/first', 'first_name'), ('http://axschema.org/namePerson/last', 'last_name'), ('http://axschema.org/namePerson/friendly', 'nickname'), --] ++ ] SREG_ATTR = [ ('email', 'email'), ('fullname', 'fullname'), @@@ -69,14 -69,14 +69,14 @@@ SESSION_NAME = 'openid USERNAME = 'username' PIPELINE = setting('SOCIAL_AUTH_PIPELINE', ( -- 'social_auth.backends.pipeline.social.social_auth_user', -- 'social_auth.backends.pipeline.associate.associate_by_email', -- 'social_auth.backends.pipeline.user.get_username', -- 'social_auth.backends.pipeline.user.create_user', -- 'social_auth.backends.pipeline.social.associate_user', -- 'social_auth.backends.pipeline.social.load_extra_data', -- 'social_auth.backends.pipeline.user.update_user_details', -- )) ++ 'social_auth.backends.pipeline.social.social_auth_user', ++ 'social_auth.backends.pipeline.associate.associate_by_email', ++ 'social_auth.backends.pipeline.user.get_username', ++ 'social_auth.backends.pipeline.user.create_user', ++ 'social_auth.backends.pipeline.social.associate_user', ++ 'social_auth.backends.pipeline.social.load_extra_data', ++ 'social_auth.backends.pipeline.user.update_user_details', ++ )) class SocialAuthBackend(ModelBackend): @@@ -258,7 -258,7 +258,7 @@@ class OpenIDBackend(SocialAuthBackend) resp = sreg.SRegResponse.fromSuccessResponse(response) if resp: values.update((alias, resp.get(name) or '') -- for name, alias in sreg_names) ++ for name, alias in sreg_names) # Use Attribute Exchange attributes if provided if ax_names: @@@ -276,9 -276,9 +276,9 @@@ # update values using SimpleRegistration or AttributeExchange # values values.update(self.values_from_response(response, -- SREG_ATTR, -- OLD_AX_ATTRS + \ -- AX_SCHEMA_ATTRS)) ++ SREG_ATTR, ++ OLD_AX_ATTRS +\ ++ AX_SCHEMA_ATTRS)) fullname = values.get('fullname') or '' first_name = values.get('first_name') or '' @@@ -294,8 -294,8 +294,8 @@@ values.update({'fullname': fullname, 'first_name': first_name, 'last_name': last_name, -- USERNAME: values.get(USERNAME) or \ -- (first_name.title() + last_name.title())}) ++ USERNAME: values.get(USERNAME) or\ ++ (first_name.title() + last_name.title())}) return values def extra_data(self, user, uid, response, details): @@@ -351,7 -351,7 +351,7 @@@ class BaseAuth(object) 'backend': self.AUTH_BACKEND.name, 'args': tuple(map(model_to_ctype, args)), 'kwargs': dict((key, model_to_ctype(val)) -- for key, val in kwargs.iteritems()) ++ for key, val in kwargs.iteritems()) } def from_session_dict(self, entry, *args, **kwargs): @@@ -363,7 -363,7 +363,7 @@@ kwargs = kwargs.copy() kwargs.update((key, ctype_to_model(val)) -- for key, val in entry['kwargs'].iteritems()) ++ for key, val in entry['kwargs'].iteritems()) return (entry['next'], args, kwargs) def continue_pipeline(self, *args, **kwargs): @@@ -425,10 -434,10 +434,10 @@@ class OpenIdAuth(BaseAuth) def auth_html(self): """Return auth HTML returned by service""" openid_request = self.setup_request(self.auth_extra_arguments()) - return_to = self.request.build_absolute_uri(self.redirect) + return_to = self.build_absolute_uri(self.redirect) form_tag = {'id': 'openid_message'} return openid_request.htmlMarkup(self.trust_root(), return_to, -- form_tag_attrs=form_tag) ++ form_tag_attrs=form_tag) def trust_root(self): """Return trust-root option""" @@@ -438,7 -446,7 +446,7 @@@ def continue_pipeline(self, *args, **kwargs): """Continue previous halted pipeline""" response = self.consumer().complete(dict(self.data.items()), - self.request.build_absolute_uri()) - self.build_absolute_uri()) ++ self.build_absolute_uri()) kwargs.update({ 'auth': self, 'response': response, @@@ -449,7 -457,7 +457,7 @@@ def auth_complete(self, *args, **kwargs): """Complete auth process""" response = self.consumer().complete(dict(self.data.items()), - self.request.build_absolute_uri()) - self.build_absolute_uri()) ++ self.build_absolute_uri()) if not response: raise AuthException(self, 'OpenID relying party endpoint') elif response.status == SUCCESS: @@@ -476,7 -484,7 +484,7 @@@ # Mark all attributes as required, Google ignores optional ones for attr, alias in (AX_SCHEMA_ATTRS + OLD_AX_ATTRS): fetch_request.add(ax.AttrInfo(attr, alias=alias, -- required=True)) ++ required=True)) else: fetch_request = sreg.SRegRequest(optional=dict(SREG_ATTR).keys()) openid_request.addExtension(fetch_request) @@@ -486,7 -494,7 +494,7 @@@ def consumer(self): """Create an OpenID Consumer object for the given Django request.""" return Consumer(self.request.session.setdefault(SESSION_NAME, {}), -- DjangoOpenIDStore()) ++ DjangoOpenIDStore()) @property def uses_redirect(self): @@@ -521,7 -532,20 +532,20 @@@ class BaseOAuth(BaseAuth) def __init__(self, request, redirect): """Init method""" super(BaseOAuth, self).__init__(request, redirect) - self.redirect_uri = self.request.build_absolute_uri(self.redirect) + self.redirect_uri = self.build_absolute_uri(self.redirect) + + def get_key_and_secret(self): + """Return tuple with Consumer Key and Consumer Secret for current + service provider. Must return (key, secret), order *must* be respected. + """ - return setting(self.SETTINGS_KEY_NAME), \ ++ return setting(self.SETTINGS_KEY_NAME),\ + setting(self.SETTINGS_SECRET_NAME) + + @classmethod + def enabled(cls): + """Return backend enabled status by checking basic settings""" - return setting(cls.SETTINGS_KEY_NAME) and \ ++ return setting(cls.SETTINGS_KEY_NAME) and\ + setting(cls.SETTINGS_SECRET_NAME) class ConsumerBasedOAuth(BaseOAuth): @@@ -580,16 -602,16 +602,16 @@@ def unauthorized_token(self): """Return request for unauthorized token (first stage)""" request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL, -- extra_params=self.request_token_extra_arguments()) ++ extra_params=self.request_token_extra_arguments()) response = self.fetch_response(request) return Token.from_string(response) def oauth_authorization_request(self, token): """Generate OAuth request to authorize token.""" return OAuthRequest.from_token_and_callback(token=token, -- callback=self.redirect_uri, -- http_url=self.AUTHORIZATION_URL, -- parameters=self.auth_extra_arguments()) ++ callback=self.redirect_uri, ++ http_url=self.AUTHORIZATION_URL, ++ parameters=self.auth_extra_arguments()) def oauth_request(self, token, url, extra_params=None): """Generate OAuth request, setups callback url""" @@@ -600,9 -622,9 +622,9 @@@ if 'oauth_verifier' in self.data: params['oauth_verifier'] = self.data['oauth_verifier'] request = OAuthRequest.from_consumer_and_token(self.consumer, -- token=token, -- http_url=url, -- parameters=params) ++ token=token, ++ http_url=url, ++ parameters=params) request.sign_request(SignatureMethod_HMAC_SHA1(), self.consumer, token) return request @@@ -680,9 -689,10 +689,10 @@@ class BaseOAuth2(BaseOAuth) 'client_id': client_id, 'client_secret': client_secret, 'redirect_uri': self.redirect_uri} - headers = {'Content-Type': 'application/x-www-form-urlencoded'} + headers = {'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json'} ++ 'Accept': 'application/json'} request = Request(self.ACCESS_TOKEN_URL, data=urlencode(params), -- headers=headers) ++ headers=headers) try: response = simplejson.loads(urlopen(request).read()) @@@ -785,4 -789,4 +789,4 @@@ def get_backend(name, *args, **kwargs) BACKENDS = { 'openid': OpenIdAuth --} ++} diff --cc social_auth/backends/contrib/mailru.py index 3f8e5dd,0000000..5e4ba15 mode 100644,000000..100644 --- a/social_auth/backends/contrib/mailru.py +++ b/social_auth/backends/contrib/mailru.py @@@ -1,100 -1,0 +1,100 @@@ +""" +Mail.ru OAuth2 support + +Take a look to http://api.mail.ru/docs/guides/oauth/ + +You need to register OAuth site here: +http://api.mail.ru/sites/my/add + +Then update your settings values using registration information + +""" + +import logging +logger = logging.getLogger(__name__) + +from django.conf import settings +from django.utils import simplejson + +from urllib import urlencode, unquote +from urllib2 import Request, urlopen, HTTPError +from hashlib import md5 + +from social_auth.backends import OAuthBackend, BaseOAuth2, USERNAME + +MAILRU_API_URL = 'http://www.appsmail.ru/platform/api' +MAILRU_OAUTH2_SCOPE = [''] + +EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires') + +class MailruBackend(OAuthBackend): + """Mail.ru authentication backend""" + name = 'mailru-oauth2' + EXTRA_DATA = [('refresh_token', 'refresh_token'), + ('expires_in', EXPIRES_NAME)] + + def get_user_id(self, details, response): + """Return user unique id provided by Mail.ru""" + return int(response['uid']) + + def get_user_details(self, response): + """Return user details from Mail.ru request""" + values = { USERNAME: unquote(response['nick']), 'email': unquote(response['email']), + 'first_name': unquote(response['first_name']), 'last_name': unquote(response['last_name'])} + + if values['first_name'] and values['last_name']: + values['fullname'] = "%s %s" % (values['first_name'], values['last_name']) + return values + + +class MailruOAuth2(BaseOAuth2): + """Mail.ru OAuth2 support""" + AUTH_BACKEND = MailruBackend + AUTHORIZATION_URL = 'https://connect.mail.ru/oauth/authorize' + ACCESS_TOKEN_URL = 'https://connect.mail.ru/oauth/token' + SETTINGS_KEY_NAME = 'MAILRU_OAUTH2_CLIENT_KEY' + SETTINGS_SECRET_NAME = 'MAILRU_OAUTH2_CLIENT_SECRET' + + def get_scope(self): + return MAILRU_OAUTH2_SCOPE + getattr(settings, 'MAILRU_OAUTH2_EXTRA_SCOPE', []) + + def auth_complete(self, *args, **kwargs): + try: + auth_result = super(MailruOAuth2, self).auth_complete(*args, **kwargs) + except HTTPError: # Mail.ru returns HTTPError 400 if cancelled + raise ValueError('Authentication cancelled') + + return auth_result + - def user_data(self, access_token): ++ def user_data(self, access_token, *args, **kwargs): + """Return user data from Mail.ru REST API""" + data = {'method': 'users.getInfo', 'session_key': access_token} + return mailru_api(data)[0] + +def mailru_sig(data): + """ Calculates signature of request data """ + + param_list = sorted(list(item + '=' + data[item] for item in data)) + + return md5(''.join(param_list) + settings.MAILRU_OAUTH2_CLIENT_SECRET).hexdigest() + +def mailru_api(data): + """ Calls Mail.ru REST API method + http://api.mail.ru/docs/guides/restapi/ + """ + data.update({'app_id': settings.MAILRU_OAUTH2_CLIENT_KEY, 'secure': '1'}) + data['sig'] = mailru_sig(data) + + params = urlencode(data) + request = Request(MAILRU_API_URL, params) + try: + return simplejson.loads(urlopen(request).read()) + except (TypeError, KeyError, IOError, ValueError, IndexError): + logger.error('Could not load data from Mail.ru.', exc_info=True, extra=dict(data=params)) + return None + + +# Backend definition +BACKENDS = { + 'mailru-oauth2': MailruOAuth2 +} diff --cc social_auth/backends/contrib/odnoklassniki.py index 09a35e6,0000000..fdd1967 mode 100644,000000..100644 --- a/social_auth/backends/contrib/odnoklassniki.py +++ b/social_auth/backends/contrib/odnoklassniki.py @@@ -1,92 -1,0 +1,93 @@@ +""" +Odnoklassniki.ru OAuth2 support + +Take a look to http://dev.odnoklassniki.ru/wiki/display/ok/The+OAuth+2.0+Protocol + +You need to register OAuth application here: +http://dev.odnoklassniki.ru/wiki/pages/viewpage.action?pageId=13992188 + +Then setup your application according manual and use information from registration +mail to set settings values + +""" + +import logging +logger = logging.getLogger(__name__) + +from django.conf import settings +from django.utils import simplejson + +from urllib import urlencode, unquote +from urllib2 import Request, urlopen +from hashlib import md5 + +from social_auth.backends import OAuthBackend, BaseOAuth2, USERNAME + +ODNOKLASSNIKI_API_URL = 'http://api.odnoklassniki.ru/fb.do' +ODNOKLASSNIKI_OAUTH2_SCOPE = [''] # Enough for authentication + +EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires') + +class OdnoklassnikiBackend(OAuthBackend): + """Odnoklassniki authentication backend""" + name = 'odnoklassniki' + EXTRA_DATA = [('refresh_token', 'refresh_token'), + ('expires_in', EXPIRES_NAME)] + + def get_user_id(self, details, response): + """Return user unique id provided by Odnoklassniki""" + return int(response['uid']) + + def get_user_details(self, response): + """Return user details from Odnoklassniki request""" ++ import pdb; pdb.set_trace() + values = { USERNAME: response['uid'], 'email': '', 'fullname': unquote(response['name']), + 'first_name': unquote(response['first_name']), 'last_name': unquote(response['last_name'])} + return values + + +class OdnoklassnikiOAuth2(BaseOAuth2): + """Odnoklassniki OAuth2 support""" + AUTH_BACKEND = OdnoklassnikiBackend + AUTHORIZATION_URL = 'http://www.odnoklassniki.ru/oauth/authorize' + ACCESS_TOKEN_URL = 'http://api.odnoklassniki.ru/oauth/token.do' + SETTINGS_KEY_NAME = 'ODNOKLASSNIKI_OAUTH2_CLIENT_KEY' + SETTINGS_SECRET_NAME = 'ODNOKLASSNIKI_OAUTH2_CLIENT_SECRET' + + def get_scope(self): + return ODNOKLASSNIKI_OAUTH2_SCOPE + getattr(settings, 'ODNOKLASSNIKI_OAUTH2_EXTRA_SCOPE', []) + - def user_data(self, access_token): ++ def user_data(self, access_token, *args, **kwargs): + """Return user data from Odnoklassniki REST API""" + data = {'access_token': access_token, 'method': 'users.getCurrentUser'} + return odnoklassniki_api(data) + +def odnoklassniki_sig(data): + """ Calculates signature of request data + access_token value must be included """ + + suffix = md5(data['access_token'] + settings.ODNOKLASSNIKI_OAUTH2_CLIENT_SECRET).hexdigest() + + check_list = sorted(list(item + '=' + data[item] for item in data if item != 'access_token')) + + return md5(''.join(check_list) + suffix).hexdigest() + +def odnoklassniki_api(data): + """ Calls Odnoklassniki REST API method + http://dev.odnoklassniki.ru/wiki/display/ok/Odnoklassniki+Rest+API + """ + data.update({'application_key': settings.ODNOKLASSNIKI_OAUTH2_APP_KEY, 'format': 'JSON'}) + data['sig'] = odnoklassniki_sig(data) + + params = urlencode(data) + request = Request(ODNOKLASSNIKI_API_URL + '?' + params) + try: + return simplejson.loads(urlopen(request).read()) + except (TypeError, KeyError, IOError, ValueError, IndexError): + logger.error('Could not load data from Odnoklassniki.', exc_info=True, extra=dict(data=params)) + return None + +# Backend definition +BACKENDS = { + 'odnoklassniki': OdnoklassnikiOAuth2 +} diff --cc social_auth/backends/contrib/vkontakte.py index 670bd7f,0f91fab..c48c580 --- a/social_auth/backends/contrib/vkontakte.py +++ b/social_auth/backends/contrib/vkontakte.py @@@ -1,243 -1,74 +1,243 @@@ """ -Vkontakte OAuth support. +VKontakte OpenAPI and OAuth 2.0 support. +This contribution adds support for VKontakte OpenAPI and OAuth 2.0 service in the form +www.vkontakte.ru. Username is retrieved from the identity returned by server. """ -from urllib import urlencode, urlopen +import logging +logger = logging.getLogger(__name__) + +from django.conf import settings +from django.contrib.auth import authenticate from django.utils import simplejson -from social_auth.utils import setting -from social_auth.backends import BaseOAuth2, OAuthBackend, USERNAME +from urllib import urlencode, unquote +from urllib2 import Request, urlopen, HTTPError +from hashlib import md5 +from time import time + +from social_auth.backends import SocialAuthBackend, OAuthBackend, BaseAuth, BaseOAuth2, USERNAME +VKONTAKTE_API_URL = 'https://api.vkontakte.ru/method/' +VKONTAKTE_SERVER_API_URL = 'http://api.vkontakte.ru/api.php' +VKONTAKTE_API_VERSION = '3.0' -# Vkontakte configuration -VK_AUTHORIZATION_URL = 'http://oauth.vk.com/authorize' -VK_ACCESS_TOKEN_URL = 'https://oauth.vk.com/access_token' -VK_USER_DATA_URL = 'https://api.vk.com/method/users.get' -VK_SERVER = 'vk.com' -VK_DEFAULT_DATA = 'first_name,last_name,screen_name,nickname' +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') -class VkontakteBackend(OAuthBackend): - """Vkontakte OAuth authentication backend""" +class VKontakteBackend(SocialAuthBackend): + """VKontakte authentication backend""" name = 'vkontakte' - 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'] + """Return user unique id provided by VKontakte""" + return int(response.GET['id']) def get_user_details(self, response): - """Return user details from Vkontakte account""" - print response - return {USERNAME: response.get('screen_name'), - 'email': '', - 'first_name': response.get('first_name'), - 'last_name': response.get('last_name')} - - -class VkontakteAuth(BaseOAuth2): - """Vkontakte OAuth mechanism""" - AUTHORIZATION_URL = VK_AUTHORIZATION_URL - ACCESS_TOKEN_URL = VK_ACCESS_TOKEN_URL - SERVER_URL = VK_SERVER - AUTH_BACKEND = VkontakteBackend - SETTINGS_KEY_NAME = 'VK_APP_ID' - SETTINGS_SECRET_NAME = 'VK_API_SECRET' - - def user_data(self, access_token, response, *args, **kwargs): - """Loads user data from service""" - fields = VK_DEFAULT_DATA - if setting('VK_EXTRA_DATA'): - fields += ',' + setting('VK_EXTRA_DATA') - - params = {'access_token': access_token, - 'fields': fields, - 'uids': response.get('user_id')} - - url = VK_USER_DATA_URL + '?' + urlencode(params) + """Return user details from VKontakte request""" + nickname = unquote(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 - try: - return simplejson.load(urlopen(url)).get('response')[0] - except (ValueError, IndexError): - return None + +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']) + + return values + + +class VKontakteAuth(BaseAuth): + """VKontakte OpenAPI authorization mechanism""" + AUTH_BACKEND = VKontakteBackend + APP_ID = settings.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, + 'VK_COMPLETE_URL': self.redirect } + + vk_template = loader.get_template(LOCAL_HTML) + context = RequestContext(self.request, dict) + + return vk_template.render(context) + + def auth_complete(self, *args, **kwargs): + """Performs check of authentication in VKontakte, returns User if succeeded""" + app_cookie = 'vk_app_' + self.APP_ID + + if not 'id' in self.request.GET or not app_cookie in self.request.COOKIES: + raise ValueError('VKontakte authentication is not completed') + + 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() + + if hash != cookie_dict['sig'] or int(cookie_dict['expire']) < time() : + raise ValueError('VKontakte authentication failed: invalid hash') + else: + kwargs.update({'response': self.request, self.AUTH_BACKEND.name: True}) + return authenticate(*args, **kwargs) + + @property + def uses_redirect(self): + """VKontakte does not require visiting server url in order + to do authentication, so auth_xxx methods are not needed to be called. + Their current implementation is just an example""" + return False + + +class VKontakteOAuth2(BaseOAuth2): + """VKontakte OAuth2 support""" + 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' def get_scope(self): - """Return list with needed access scope""" - # 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 - return setting('VK_EXTRA_SCOPE', []) + return VKONTAKTE_OAUTH2_SCOPE + getattr(settings, 'VKONTAKTE_OAUTH2_EXTRA_SCOPE', []) + + def auth_complete(self, *args, **kwargs): + if USE_APP_AUTH: + stop, app_auth = self.application_auth() + + if app_auth: + return app_auth + + 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): ++ def user_data(self, access_token, *args, **kwargs): + """Return user data from VKontakte API""" + data = {'access_token': access_token } + + return vkontakte_api('getUserInfoEx', data) + + def user_profile(self, user_id, access_token = None): + data = {'uids': user_id, 'fields': 'photo'} + + if access_token: + data['access_token'] = access_token + + profiles = vkontakte_api('getProfiles', data).get('response', None) + + return profiles[0] if profiles else None + + def is_app_user(self, user_id, access_token = None): + """Returns app usage flag from VKontakte API""" + + data = {'uid': user_id} + + if access_token: + data['access_token'] = access_token + + return vkontakte_api('isAppUser', data).get('response', 0) + + def application_auth(self): + required_params = ('is_app_user', 'viewer_id', 'access_token', 'api_id', ) + + for param in required_params: + if not param in self.request.REQUEST: + return (False, None,) + + auth_key = self.request.REQUEST.get('auth_key') + + # 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() + if check_key != auth_key: + raise ValueError('VKontakte authentication failed: invalid auth key') + + user_check = USE_APP_AUTH.get('user_mode', 0) + user_id = self.request.REQUEST.get('viewer_id') + + if user_check: + is_user = self.request.REQUEST.get('is_app_user') if user_check == 1 else self.is_app_user(user_id) + + if not int(is_user): + return (True, None,) + + data = {'response': self.user_profile(user_id), 'user_id': user_id} + + return (True, authenticate(**{'response': data, self.AUTH_BACKEND.name: True})) + + +def vkontakte_api(method, data): + """ Calls VKontakte OpenAPI method + http://vkontakte.ru/apiclub, + http://vkontakte.ru/pages.php?o=-1&p=%C2%FB%EF%EE%EB%ED%E5%ED%E8%E5%20%E7%E0%EF%F0%EE%F1%EE%E2%20%EA%20API + """ + + # We need to perform server-side call if no access_token + if not 'access_token' in data: + if not 'v' in 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['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 + + param_list = sorted(list(item + '=' + data[item] for item in data)) + data['sig'] = md5(''.join(param_list) + secret).hexdigest() + else: + url = VKONTAKTE_API_URL + method + + params = urlencode(data) + api_request = Request(url + '?' + params) + try: + return simplejson.loads(urlopen(api_request).read()) + except (TypeError, KeyError, IOError, ValueError, IndexError): + logger.error('Could not load data from VKontakte.', exc_info=True, extra=dict(data=params)) + return None # Backend definition diff --cc social_auth/backends/contrib/yandex.py index f5bcb66,24414c6..6cffc9d --- a/social_auth/backends/contrib/yandex.py +++ b/social_auth/backends/contrib/yandex.py @@@ -75,57 -42,46 +75,57 @@@ class YandexAuth(OpenIdAuth) AUTH_BACKEND = YandexBackend def openid_url(self): - """Return Google OpenID service url""" - return YANDEX_OPENID_URL + """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' + + 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 user_data(self, access_token): ++ 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 }) - def get_user_details(self, response): - """Return user details from Vkontakte account""" - return { USERNAME: get_username_from_url(response.get('links')), - 'email': response.get('email'), - 'first_name': response.get('name'), - } - - -class YaruAuth(BaseOAuth2): - """Yandex OAuth mechanism""" - AUTHORIZATION_URL = YANDEX_AUTHORIZATION_URL - ACCESS_TOKEN_URL = YANDEX_ACCESS_TOKEN_URL - SERVER_URL = YANDEX_SERVER - AUTH_BACKEND = YaruBackend - SETTINGS_KEY_NAME = 'YANDEX_APP_ID' - SETTINGS_SECRET_NAME = 'YANDEX_API_SECRET' - - def user_data(self, access_token, response, *args, **kwargs): - """Loads user data from service""" - params = {'oauth_token': access_token, - 'format': 'json', - } - headers = {'Content-Type': 'application/x-yaru+json; type=person', - 'Accept': 'application/x-yaru+json'} - - url = YANDEX_USER_ID_URL + '?' + urlencode(params) try: - return simplejson.load(urlopen(url)) - except (ValueError, IndexError): + 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): + logger.error('Could not load data from Yandex.', exc_info=True, extra=dict(data=params)) return None diff --cc social_auth/backends/facebook.py index b7abee6,b1fbbbc..06d69d9 --- a/social_auth/backends/facebook.py +++ b/social_auth/backends/facebook.py @@@ -88,51 -85,7 +89,59 @@@ class FacebookAuth(BaseOAuth2) def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" - if 'code' not in self.data: + access_token = None + expires = None + + if 'code' in self.data: + url = ACCESS_TOKEN + urlencode({ + 'client_id': setting('FACEBOOK_APP_ID'), + 'redirect_uri': self.redirect_uri, + 'client_secret': setting('FACEBOOK_API_SECRET'), + 'code': self.data['code'] + }) + try: + response = cgi.parse_qs(urlopen(url).read()) + except HTTPError: + raise AuthFailed(self, 'There was an error authenticating the app') + + access_token = response['access_token'][0] + if 'expires' in response: + expires = response['expires'][0] + + if 'signed_request' in self.data: + response = load_signed_request(self.data.get('signed_request')) + + if response is not None: + access_token = response.get('access_token') or response.get('oauth_token') \ + or self.data.get('access_token') + + if 'expires' in response: + expires = response['expires'] + + if access_token: + data = self.user_data(access_token) + - if data is not None: - data['access_token'] = access_token - # expires will not be part of response if offline access - # premission was requested - if expires: - data['expires'] = response['expires'][0] ++ if not isinstance(data, dict): ++ # From time to time Facebook responds back a JSON with just False ++ # as value, the reason is still unknown, but since the data is ++ # needed (it contains the user ID used to identify the account on ++ # further logins), this app cannot allow it to continue with the ++ # auth process. ++ raise AuthUnknownError(self, 'An error ocurred while retrieving '\ ++ 'users Facebook data') ++ ++ data['access_token'] = access_token ++ # expires will not be part of response if offline access ++ # premission was requested ++ if expires: ++ data['expires'] = response['expires'][0] + + kwargs.update({'auth': self, + 'response': data, + self.AUTH_BACKEND.name: True}) + + return authenticate(*args, **kwargs) + else: if self.data.get('error') == 'access_denied': raise AuthCanceled(self) else: diff --cc social_auth/views.py index 4895888,13bd330..f9ff07f --- a/social_auth/views.py +++ b/social_auth/views.py @@@ -13,75 -9,16 +9,16 @@@ from django.http import HttpResponseRed from django.contrib.auth import login, REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_required from django.contrib import messages - from django.utils.importlib import import_module from django.views.decorators.csrf import csrf_exempt - from social_auth.backends import get_backend - from social_auth.utils import sanitize_redirect, setting, log, \ -from social_auth.utils import sanitize_redirect, setting, \ -- backend_setting, clean_partial_pipeline ++from social_auth.utils import sanitize_redirect, setting,\ ++ backend_setting, clean_partial_pipeline + from social_auth.decorators import dsa_view - from social_auth.backends.exceptions import AuthFailed, AuthException --DEFAULT_REDIRECT = setting('SOCIAL_AUTH_LOGIN_REDIRECT_URL') or \ ++DEFAULT_REDIRECT = setting('SOCIAL_AUTH_LOGIN_REDIRECT_URL') or\ setting('LOGIN_REDIRECT_URL') LOGIN_ERROR_URL = setting('LOGIN_ERROR_URL', setting('LOGIN_URL')) - RAISE_EXCEPTIONS = setting('SOCIAL_AUTH_RAISE_EXCEPTIONS', setting('DEBUG')) - PROCESS_EXCEPTIONS = setting('SOCIAL_AUTH_PROCESS_EXCEPTIONS', - 'social_auth.utils.log_exceptions_to_messages') - - - def dsa_view(redirect_name=None): - """Decorate djangos-social-auth views. Will check and retrieve backend - or return HttpResponseServerError if backend is not found. - - redirect_name parameter is used to build redirect URL used by backend. - """ - def dec(func): - @wraps(func) - def wrapper(request, backend, *args, **kwargs): - if redirect_name: - redirect = reverse(redirect_name, args=(backend,)) - else: - redirect = request.path - backend = get_backend(backend, request, redirect) - - if not backend: - return HttpResponseServerError('Incorrect authentication ' + \ - 'service') - - try: - return func(request, backend, *args, **kwargs) - except AuthException, e: - backend_name = backend.AUTH_BACKEND.name - if 'django.contrib.messages' in setting('INSTALLED_APPS'): - from django.contrib.messages.api import error - error(request, unicode(e), extra_tags=backend_name) - else: - log('warn', 'Messages framework not in place, some '+ - 'errors have not been shown to the user.') - url = setting('SOCIAL_AUTH_BACKEND_ERROR_URL', LOGIN_ERROR_URL) - return HttpResponseRedirect(url) - except Exception, e: # some error ocurred - if RAISE_EXCEPTIONS: - raise - log('error', unicode(e), exc_info=True, extra={ - 'request': request - }) - - mod, func_name = PROCESS_EXCEPTIONS.rsplit('.', 1) - try: - process = getattr(import_module(mod), func_name, - lambda *args: None) - except ImportError: - pass - else: - process(request, backend, e) - - url = backend_setting(backend, 'SOCIAL_AUTH_BACKEND_ERROR_URL', - LOGIN_ERROR_URL) - return HttpResponseRedirect(url) - return wrapper - return dec @dsa_view(setting('SOCIAL_AUTH_COMPLETE_URL_NAME', 'socialauth_complete')) @@@ -121,8 -51,8 +51,8 @@@ def associate_complete(request, backend return user else: url = backend_setting(backend, -- 'SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL') or \ -- redirect_value or \ ++ 'SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL') or\ ++ redirect_value or\ DEFAULT_REDIRECT return HttpResponseRedirect(url) @@@ -132,8 -62,8 +62,8 @@@ def disconnect(request, backend, association_id=None): """Disconnects given backend from current logged in user.""" backend.disconnect(request.user, association_id) -- url = request.REQUEST.get(REDIRECT_FIELD_NAME, '') or \ -- backend_setting(backend, 'SOCIAL_AUTH_DISCONNECT_REDIRECT_URL') or \ ++ url = request.REQUEST.get(REDIRECT_FIELD_NAME, '') or\ ++ backend_setting(backend, 'SOCIAL_AUTH_DISCONNECT_REDIRECT_URL') or\ DEFAULT_REDIRECT return HttpResponseRedirect(url) @@@ -156,7 -86,7 +86,7 @@@ def auth_process(request, backend) return HttpResponseRedirect(backend.auth_url()) else: return HttpResponse(backend.auth_html(), -- content_type='text/html;charset=UTF-8') ++ content_type='text/html;charset=UTF-8') def complete_process(request, backend, *args, **kwargs): @@@ -178,7 -108,7 +108,7 @@@ # in authenticate process social_user = user.social_user if redirect_value: -- request.session[REDIRECT_FIELD_NAME] = redirect_value or \ ++ request.session[REDIRECT_FIELD_NAME] = redirect_value or\ DEFAULT_REDIRECT if setting('SOCIAL_AUTH_SESSION_EXPIRATION', True): @@@ -190,23 -120,23 +120,23 @@@ # store last login backend name in session key = setting('SOCIAL_AUTH_LAST_LOGIN', -- 'social_auth_last_login_backend') ++ 'social_auth_last_login_backend') request.session[key] = social_user.provider # Remove possible redirect URL from session, if this is a new # account, send him to the new-users-page if defined. new_user_redirect = backend_setting(backend, -- 'SOCIAL_AUTH_NEW_USER_REDIRECT_URL') ++ 'SOCIAL_AUTH_NEW_USER_REDIRECT_URL') if new_user_redirect and getattr(user, 'is_new', False): url = new_user_redirect else: -- url = redirect_value or \ ++ url = redirect_value or\ backend_setting(backend, -- 'SOCIAL_AUTH_LOGIN_REDIRECT_URL') or \ ++ 'SOCIAL_AUTH_LOGIN_REDIRECT_URL') or\ DEFAULT_REDIRECT else: url = backend_setting(backend, 'SOCIAL_AUTH_INACTIVE_USER_URL', -- LOGIN_ERROR_URL) ++ LOGIN_ERROR_URL) else: msg = setting('LOGIN_ERROR_MESSAGE', None) if msg: @@@ -224,8 -154,9 +154,9 @@@ def auth_complete(request, backend, use if request.session.get(name): data = request.session.pop(name) idx, args, kwargs = backend.from_session_dict(data, user=user, -- request=request, -- *args, **kwargs) ++ request=request, ++ *args, **kwargs) return backend.continue_pipeline(pipeline_index=idx, *args, **kwargs) else: - return backend.auth_complete(user=user, request=request, *args, **kwargs) + return backend.auth_complete(user=user, request=request, *args, - **kwargs) ++ **kwargs)