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
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',
'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.vkontakte.VkontakteBackend',
+ 'social_auth.backends.contrib.github.GithubBackend',
- 'social_auth.backends.contrib.yandex.YaruBackend',
+ 'social_auth.backends.contrib.yandex.YandexBackend',
'social_auth.backends.OpenIDBackend',
'social_auth.backends.contrib.livejournal.LiveJournalBackend',
'social_auth.backends.browserid.BrowserIDBackend',
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
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'):
('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'),
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):
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:
# 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 ''
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):
'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):
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):
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"""
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,
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:
# 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)
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):
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):
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"""
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
'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())
BACKENDS = {
'openid': OpenIdAuth
--}
++}
--- /dev/null
- def user_data(self, access_token):
+"""
+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, *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
+}
--- /dev/null
- def user_data(self, access_token):
+"""
+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, *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
+}
"""
-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
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
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:
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'))
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)
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)
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):
# 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):
# 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:
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)