* `Google OpenID`_
* `Google OAuth`_
* `Yahoo OpenID`_
- * `LiveJournal OpenID`_
* OpenId_ like myOpenID_
* `Twitter OAuth`_
* `Facebook OAuth`_
+
+ Some contributions added support for:
+
+ * `LiveJournal OpenID`_
* `Orkut OAuth`_
- Basic user data population and signaling, to allows custom fields values
- Add desired authentication backends to AUTHENTICATION_BACKENDS_ setting::
AUTHENTICATION_BACKENDS = (
- 'social_auth.backends.TwitterBackend',
- 'social_auth.backends.FacebookBackend',
- 'social_auth.backends.OrkutBackend',
- 'social_auth.backends.GoogleOAuthBackend',
- 'social_auth.backends.GoogleBackend',
- 'social_auth.backends.YahooBackend',
- 'social_auth.backends.LiveJournalBackend',
+ 'social_auth.backends.twitter.TwitterBackend',
+ 'social_auth.backends.facebook.FacebookBackend',
+ 'social_auth.backends.google.GoogleOAuthBackend',
+ 'social_auth.backends.google.GoogleBackend',
+ 'social_auth.backends.yahoo.YahooBackend',
+ 'social_auth.backends.contrib.LiveJournalBackend',
+ 'social_auth.backends.contrib.orkut.OrkutBackend',
'social_auth.backends.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
+ Note: this was introduced in a recent change and it's not backward
+ compatible, take into account that saved sessions won't be able to login
+ because the backend string stored in session (like backends.TwitterBackend)
+ won't match the new paths.
+
+- The app will try to import custom backends from the sources defined in::
+
+ SOCIAL_AUTH_IMPORT_BACKENDS = (
+ 'myproy.social_auth_extra_services',
+ )
+
+ This way it's easier to add new providers, check the already defined ones
+ in social_auth.backends for examples.
+
+ Take into account that backends must be defined in AUTHENTICATION_BACKENDS_
+ or Django won't pick them when trying to authenticate the user.
+
- Setup Twitter, Facebook, Orkut and Google OAuth keys (see OAuth_ section
for details)::
- TWITTER_CONSUMER_KEY = ''
- TWITTER_CONSUMER_SECRET = ''
- FACEBOOK_APP_ID = ''
- FACEBOOK_API_SECRET = ''
- ORKUT_CONSUMER_KEY = ''
- ORKUT_CONSUMER_SECRET = ''
+ TWITTER_CONSUMER_KEY = ''
+ TWITTER_CONSUMER_SECRET = ''
+ FACEBOOK_APP_ID = ''
+ FACEBOOK_API_SECRET = ''
+ ORKUT_CONSUMER_KEY = ''
+ ORKUT_CONSUMER_SECRET = ''
GOOGLE_CONSUMER_KEY = ''
GOOGLE_CONSUMER_SECRET = ''
)
AUTHENTICATION_BACKENDS = (
- 'social_auth.backends.TwitterBackend',
- 'social_auth.backends.FacebookBackend',
- 'social_auth.backends.GoogleOAuthBackend',
- 'social_auth.backends.GoogleBackend',
- 'social_auth.backends.YahooBackend',
+ 'social_auth.backends.twitter.TwitterBackend',
+ 'social_auth.backends.facebook.FacebookBackend',
+ 'social_auth.backends.google.GoogleOAuthBackend',
+ 'social_auth.backends.google.GoogleBackend',
+ 'social_auth.backends.yahoo.YahooBackend',
'social_auth.backends.OpenIDBackend',
- 'social_auth.backends.LiveJournalBackend',
+ 'social_auth.backends.contrib.livejournal.LiveJournalBackend',
'django.contrib.auth.backends.ModelBackend',
)
<a rel="nofollow" href="/associate/yahoo/">Yahoo</a>
{% if yahoo %}<span class="associated">(associated)</span>{% endif %}
</li>
-
+ <li>
+ <form action="/login/livejournal/" method="post">{% csrf_token %}
+ <div>
+ <label for="openid_lj_user">LiveJournal user
+ {% if livejournal %} <span class="associated">(associated)</span>{% endif %}:
+ </label>
+ <input id="openid_lj_user" type="text" value="" name="openid_lj_user" />
+ <input type="submit" value="Login"/>
+ </div>
+ </form>
+ </li>
<li>
<form action="/associate/openid/" method="post">{% csrf_token %}
<div>
<ul>
<li><a rel="nofollow" href="/login/google/">Google</a></li>
<li><a rel="nofollow" href="/login/yahoo/">Yahoo</a></li>
- <li>
- <form action="/login/livejournal/" method="post">{% csrf_token %}
- <div>
+ <li>
+ <form action="/login/livejournal/" method="post">{% csrf_token %}
+ <div>
<label for="openid_lj_user">LiveJournal user:</label>
<input id="openid_lj_user" type="text" value="" name="openid_lj_user" />
<input type="submit" value="Login"/>
- </div>
+ </div>
</form>
</li>
<li>
Django-social-auth application, allows OpenId or OAuth user
registration/authentication just adding a few configurations.
"""
-version = (0, 1, 7)
+version = (0, 2, 0)
__version__ = '.'.join(map(str, version))
"""Admin settings"""
from django.contrib import admin
-from .models import UserSocialAuth, Nonce, Association
+from social_auth.models import UserSocialAuth, Nonce, Association
class UserSocialAuthOption(admin.ModelAdmin):
+++ /dev/null
-"""Authentication handling class"""
-import cgi
-import urllib
-import urllib2
-import httplib
-
-from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE
-from openid.consumer.discover import DiscoveryFailure
-from openid.extensions import sreg, ax
-from oauth.oauth import OAuthConsumer, OAuthToken, OAuthRequest, \
- OAuthSignatureMethod_HMAC_SHA1
-
-from django.conf import settings
-from django.utils import simplejson
-from django.contrib.auth import authenticate
-
-from .store import DjangoOpenIDStore
-from .backends import TwitterBackend, OrkutBackend, FacebookBackend, \
- OpenIDBackend, GoogleBackend, YahooBackend, \
- GoogleOAuthBackend, LiveJournalBackend
-from .conf import AX_ATTRS, SREG_ATTR, OPENID_ID_FIELD, SESSION_NAME, \
- OPENID_GOOGLE_URL, OPENID_YAHOO_URL, TWITTER_SERVER, \
- OPENID_LIVEJOURNAL_URL, OPENID_LIVEJOURNAL_USER_FIELD, \
- TWITTER_REQUEST_TOKEN_URL, TWITTER_ACCESS_TOKEN_URL, \
- TWITTER_AUTHORIZATION_URL, TWITTER_CHECK_AUTH, \
- FACEBOOK_CHECK_AUTH, FACEBOOK_AUTHORIZATION_URL, \
- FACEBOOK_ACCESS_TOKEN_URL, GOOGLE_REQUEST_TOKEN_URL, \
- GOOGLE_ACCESS_TOKEN_URL, GOOGLE_AUTHORIZATION_URL, \
- GOOGLE_SERVER, GOOGLE_OAUTH_SCOPE, GOOGLEAPIS_EMAIL, \
- ORKUT_REST_ENDPOINT, ORKUT_DEFAULT_DATA, ORKUT_SCOPE
-
-
-class BaseAuth(object):
- """Base authentication class, new authenticators should subclass
- and implement needed methods"""
- def __init__(self, request, redirect):
- self.request = request
- self.redirect = redirect
-
- def auth_url(self):
- """Must return redirect URL to auth provider"""
- raise NotImplementedError('Implement in subclass')
-
- def auth_html(self):
- """Must return login HTML content returned by provider"""
- raise NotImplementedError('Implement in subclass')
-
- def auth_complete(self, *args, **kwargs):
- """Completes loging process, must return user instance"""
- raise NotImplementedError('Implement in subclass')
-
- @property
- def uses_redirect(self):
- """Return True if this provider uses redirect url method,
- otherwise return false."""
- return True
-
-
-class OpenIdAuth(BaseAuth):
- """
- OpenId process handling
- @AUTH_BACKEND Authorization backend related with this service
- """
- AUTH_BACKEND = OpenIDBackend
-
- def auth_url(self):
- openid_request = self.setup_request()
- # Construct completion URL, including page we should redirect to
- return_to = self.request.build_absolute_uri(self.redirect)
- trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
- self.request.build_absolute_uri('/'))
- return openid_request.redirectURL(trust_root, return_to)
-
- def auth_html(self):
- openid_request = self.setup_request()
- return_to = self.request.build_absolute_uri(self.redirect)
- trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
- self.request.build_absolute_uri('/'))
- form_tag = {'id': 'openid_message'}
- return openid_request.htmlMarkup(trust_root, return_to,
- form_tag_attrs=form_tag)
-
- def auth_complete(self, *args, **kwargs):
- response = self.consumer().complete(dict(self.request.REQUEST.items()),
- self.request.build_absolute_uri())
- if not response:
- raise ValueError('This is an OpenID relying party endpoint')
- elif response.status == SUCCESS:
- kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
- return authenticate(*args, **kwargs)
- elif response.status == FAILURE:
- raise ValueError('OpenID authentication failed: %s' % \
- response.message)
- elif response.status == CANCEL:
- raise ValueError('Authentication cancelled')
- else:
- raise ValueError('Unknown OpenID response type: %r' % \
- response.status)
-
- def setup_request(self):
- """Setup request"""
- openid_request = self.openid_request()
- # Request some user details. Use attribute exchange if provider
- # advertises support.
- if openid_request.endpoint.supportsType(ax.AXMessage.ns_uri):
- fetch_request = ax.FetchRequest()
- # Mark all attributes as required, Google ignores optional ones
- for attr, alias in AX_ATTRS:
- fetch_request.add(ax.AttrInfo(attr, alias=alias,
- required=True))
- else:
- fetch_request = sreg.SRegRequest(optional=SREG_ATTR)
- openid_request.addExtension(fetch_request)
-
- return openid_request
-
- def consumer(self):
- """Create an OpenID Consumer object for the given Django request."""
- return Consumer(self.request.session.setdefault(SESSION_NAME, {}),
- DjangoOpenIDStore())
-
- @property
- def uses_redirect(self):
- """Return true if openid request will be handled with redirect or
- HTML content will be returned.
- """
- if not hasattr(self, '_uses_redirect'):
- setattr(self, '_uses_redirect',
- self.openid_request().shouldSendRedirect())
- return getattr(self, '_uses_redirect', True)
-
- def openid_request(self):
- """Return openid request"""
- if not hasattr(self, '_openid_request'):
- openid_url = self.openid_url()
- try:
- openid_request = self.consumer().begin(openid_url)
- except DiscoveryFailure, err:
- raise ValueError('OpenID discovery error: %s' % err)
- else:
- setattr(self, '_openid_request', openid_request)
- return getattr(self, '_openid_request', None)
-
- def openid_url(self):
- """Return service provider URL.
- This base class is generic accepting a POST parameter that specifies
- provider URL."""
- if self.request.method != 'POST' or \
- OPENID_ID_FIELD not in self.request.POST:
- raise ValueError('Missing openid identifier')
- return self.request.POST[OPENID_ID_FIELD]
-
-
-class GoogleAuth(OpenIdAuth):
- """Google OpenID authentication"""
- AUTH_BACKEND = GoogleBackend
-
- def openid_url(self):
- """Return Google OpenID service url"""
- return OPENID_GOOGLE_URL
-
-
-class YahooAuth(OpenIdAuth):
- """Yahoo OpenID authentication"""
- AUTH_BACKEND = YahooBackend
-
- def openid_url(self):
- """Return Yahoo OpenID service url"""
- return OPENID_YAHOO_URL
-
-
-class LiveJournalAuth(OpenIdAuth):
- """LiveJournal OpenID authentication"""
- AUTH_BACKEND = LiveJournalBackend
-
- def uses_redirect(self):
- """LiveJournal uses redirect"""
- return True
-
- def openid_url(self):
- """Returns LiveJournal authentication URL"""
- if self.request.method != 'POST' or \
- not self.request.POST.get(OPENID_LIVEJOURNAL_USER_FIELD):
- raise ValueError, 'Missing LiveJournal user identifier'
- return OPENID_LIVEJOURNAL_URL % \
- self.request.POST[OPENID_LIVEJOURNAL_USER_FIELD]
-
-
-class BaseOAuth(BaseAuth):
- """OAuth base class"""
- def __init__(self, request, redirect):
- """Init method"""
- super(BaseOAuth, self).__init__(request, redirect)
- self.redirect_uri = self.request.build_absolute_uri(self.redirect)
-
-
-class ConsumerBasedOAuth(BaseOAuth):
- """Consumer based mechanism OAuth authentication, fill the needed
- parameters to communicate properly with authentication service.
-
- @AUTHORIZATION_URL Authorization service url
- @REQUEST_TOKEN_URL Request token URL
- @ACCESS_TOKEN_URL Access token URL
- @SERVER_URL Authorization server URL
- @AUTH_BACKEND Authorization backend related with
- this service
- """
- AUTHORIZATION_URL = ''
- REQUEST_TOKEN_URL = ''
- ACCESS_TOKEN_URL = ''
- SERVER_URL = ''
- AUTH_BACKEND = None
-
- def auth_url(self):
- """Returns redirect url"""
- token = self.unauthorized_token()
- name = self.AUTH_BACKEND.name + 'unauthorized_token_name'
- self.request.session[name] = token.to_string()
- return self.oauth_request(token, self.AUTHORIZATION_URL).to_url()
-
- def auth_complete(self, *args, **kwargs):
- """Returns user, might be logged in"""
- name = self.AUTH_BACKEND.name + 'unauthorized_token_name'
- unauthed_token = self.request.session.get(name)
- if not unauthed_token:
- raise ValueError('Missing unauthorized token')
-
- token = OAuthToken.from_string(unauthed_token)
- if token.key != self.request.GET.get('oauth_token', 'no-token'):
- raise ValueError('Incorrect tokens')
-
- access_token = self.access_token(token)
- data = self.user_data(access_token)
- if data is not None:
- data['access_token'] = access_token.to_string()
-
- kwargs.update({'response': data, self.AUTH_BACKEND.name: True})
- return authenticate(*args, **kwargs)
-
- def unauthorized_token(self):
- """Return request for unauthorized token (first stage)"""
- request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL)
- response = self.fetch_response(request)
- return OAuthToken.from_string(response)
-
- def oauth_request(self, token, url, extra_params=None):
- """Generate OAuth request, setups callback url"""
- params = {'oauth_callback': self.redirect_uri}
- if extra_params:
- params.update(extra_params)
-
- if 'oauth_verifier' in self.request.GET:
- params['oauth_verifier'] = self.request.GET['oauth_verifier']
- request = OAuthRequest.from_consumer_and_token(self.consumer,
- token=token,
- http_url=url,
- parameters=params)
- request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer,
- token)
- return request
-
- def fetch_response(self, request):
- """Executes request and fetchs service response"""
- self.connection.request(request.http_method, request.to_url())
- response = self.connection.getresponse()
- return response.read()
-
- def access_token(self, token):
- """Return request for access token value"""
- request = self.oauth_request(token, self.ACCESS_TOKEN_URL)
- return OAuthToken.from_string(self.fetch_response(request))
-
- def user_data(self, access_token):
- """Loads user data from service"""
- raise NotImplementedError('Implement in subclass')
-
- @property
- def connection(self):
- """Setups connection"""
- conn = getattr(self, '_connection', None)
- if conn is None:
- conn = httplib.HTTPSConnection(self.SERVER_URL)
- setattr(self, '_connection', conn)
- return conn
-
- @property
- def consumer(self):
- """Setups consumer"""
- cons = getattr(self, '_consumer', None)
- if cons is None:
- cons = OAuthConsumer(*self.get_key_and_secret())
- setattr(self, '_consumer', cons)
- return cons
-
- 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.
- """
- raise NotImplementedError('Implement in subclass')
-
-
-class BaseGoogleOAuth(ConsumerBasedOAuth):
- """Base class for Google OAuth mechanism"""
- AUTHORIZATION_URL = GOOGLE_AUTHORIZATION_URL
- REQUEST_TOKEN_URL = GOOGLE_REQUEST_TOKEN_URL
- ACCESS_TOKEN_URL = GOOGLE_ACCESS_TOKEN_URL
- SERVER_URL = GOOGLE_SERVER
- AUTH_BACKEND = None
-
- def user_data(self, access_token):
- """Loads user data from G service"""
- raise NotImplementedError('Implement in subclass')
-
- def get_key_and_secret(self):
- """Return Consumer Key and Consumer Secret pair"""
- raise NotImplementedError('Implement in subclass')
-
-
-class OrkutAuth(BaseGoogleOAuth):
- """Orkut OAuth authentication mechanism"""
- AUTH_BACKEND = OrkutBackend
-
- def user_data(self, access_token):
- """Loads user data from Orkut service"""
- fields = ORKUT_DEFAULT_DATA
- if hasattr(settings, 'ORKUT_EXTRA_DATA'):
- fields += ',' + settings.ORKUT_EXTRA_DATA
- scope = ORKUT_SCOPE + \
- getattr(settings, 'ORKUT_EXTRA_SCOPE', [])
- params = {'method': 'people.get',
- 'id': 'myself',
- 'userId': '@me',
- 'groupId': '@self',
- 'fields': fields,
- 'scope': ' '.join(scope)}
- request = self.oauth_request(access_token, ORKUT_REST_ENDPOINT, params)
- response = urllib.urlopen(request.to_url()).read()
- try:
- return simplejson.loads(response)['data']
- except (simplejson.JSONDecodeError, KeyError):
- return None
-
- def get_key_and_secret(self):
- """Return Orkut Consumer Key and Consumer Secret pair"""
- return settings.ORKUT_CONSUMER_KEY, settings.ORKUT_CONSUMER_SECRET
-
-
-class GoogleOAuth(BaseGoogleOAuth):
- """Google OAuth authorization mechanism"""
- AUTH_BACKEND = GoogleOAuthBackend
-
- def user_data(self, access_token):
- """Loads user data data from googleapis service, only email so far
- as it's described in:
- http://sites.google.com/site/oauthgoog/Home/emaildisplayscope
- OAuth parameters needs to be passed in the queryset and
- Authorization header, this behavior is listed in:
- http://groups.google.com/group/oauth/browse_thread/thread/d15add9beb418ebc
- """
- url = self.oauth_request(access_token, GOOGLEAPIS_EMAIL,
- {'alt': 'json'}).to_url()
- params = url.split('?', 1)[1]
- request = urllib2.Request(url)
- request.headers['Authorization'] = params # setup header
- response = urllib2.urlopen(request).read()
- try:
- return simplejson.loads(response)['data']
- except (simplejson.JSONDecodeError, KeyError):
- return None
-
- def oauth_request(self, token, url, extra_params=None):
- extra_params = extra_params or {}
- scope = GOOGLE_OAUTH_SCOPE + \
- getattr(settings, 'GOOGLE_OAUTH_EXTRA_SCOPE', [])
- extra_params.update({
- 'scope': ' '.join(scope),
- 'xoauth_displayname': getattr(settings, 'GOOGLE_DISPLAY_NAME',
- 'Social Auth')
- })
- return super(GoogleOAuth, self).oauth_request(token, url, extra_params)
-
- def get_key_and_secret(self):
- """Return Google OAuth Consumer Key and Consumer Secret pair, uses
- anonymous by default, beware that this marks the application as not
- registered and a security badge is displayed on authorization page.
- http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth
- """
- return getattr(settings, 'GOOGLE_CONSUMER_KEY', 'anonymous'), \
- getattr(settings, 'GOOGLE_CONSUMER_SECRET', 'anonymous')
-
-
-class TwitterAuth(ConsumerBasedOAuth):
- """Twitter OAuth authentication mechanism"""
- AUTHORIZATION_URL = TWITTER_AUTHORIZATION_URL
- REQUEST_TOKEN_URL = TWITTER_REQUEST_TOKEN_URL
- ACCESS_TOKEN_URL = TWITTER_ACCESS_TOKEN_URL
- SERVER_URL = TWITTER_SERVER
- AUTH_BACKEND = TwitterBackend
-
- def user_data(self, access_token):
- """Return user data provided"""
- request = self.oauth_request(access_token, TWITTER_CHECK_AUTH)
- json = self.fetch_response(request)
- try:
- return simplejson.loads(json)
- except simplejson.JSONDecodeError:
- return None
-
- def get_key_and_secret(self):
- """Return Twitter Consumer Key and Consumer Secret pair"""
- return settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET
-
-
-class FacebookAuth(BaseOAuth):
- """Facebook OAuth mechanism"""
-
- def auth_url(self):
- """Returns redirect url"""
- args = {'client_id': settings.FACEBOOK_APP_ID,
- 'redirect_uri': self.redirect_uri}
- if hasattr(settings, 'FACEBOOK_EXTENDED_PERMISSIONS'):
- args['scope'] = ','.join(settings.FACEBOOK_EXTENDED_PERMISSIONS)
- return FACEBOOK_AUTHORIZATION_URL + '?' + urllib.urlencode(args)
-
- def auth_complete(self, *args, **kwargs):
- """Returns user, might be logged in"""
- if 'code' in self.request.GET:
- url = FACEBOOK_ACCESS_TOKEN_URL + '?' + \
- urllib.urlencode({'client_id': settings.FACEBOOK_APP_ID,
- 'redirect_uri': self.redirect_uri,
- 'client_secret': settings.FACEBOOK_API_SECRET,
- 'code': self.request.GET['code']})
- response = cgi.parse_qs(urllib.urlopen(url).read())
-
- access_token = response['access_token'][0]
- data = self.user_data(access_token)
- if data is not None:
- if 'error' in data:
- raise ValueError('Authentication error')
- data['access_token'] = access_token
-
- kwargs.update({'response': data, FacebookBackend.name: True})
- return authenticate(*args, **kwargs)
- else:
- raise ValueError('Authentication error')
-
- def user_data(self, access_token):
- """Loads user data from service"""
- params = {'access_token': access_token}
- url = FACEBOOK_CHECK_AUTH + '?' + urllib.urlencode(params)
- try:
- return simplejson.load(urllib.urlopen(url))
- except simplejson.JSONDecodeError:
- return None
-
-
-# Authentication backends
-BACKENDS = {
- 'twitter': TwitterAuth,
- 'facebook': FacebookAuth,
- 'google': GoogleAuth,
- 'google-oauth': GoogleOAuth,
- 'yahoo': YahooAuth,
- 'livejournal': LiveJournalAuth,
- 'orkut': OrkutAuth,
- 'openid': OpenIdAuth,
-}
-
-def get_backend(name, *args, **kwargs):
- """Return auth backend instance *if* it's registered, None in other case"""
- return BACKENDS.get(name, lambda *args, **kwargs: None)(*args, **kwargs)
+++ /dev/null
-"""
-Authentication backeds for django.contrib.auth AUTHENTICATION_BACKENDS setting
-"""
-import urlparse
-from os import urandom
-
-from openid.extensions import ax, sreg
-
-from django.conf import settings
-from django.contrib.auth.backends import ModelBackend
-from django.utils.hashcompat import md5_constructor
-
-from .models import UserSocialAuth
-from .conf import OLD_AX_ATTRS, AX_SCHEMA_ATTRS
-from .signals import pre_update
-
-USERNAME = 'username'
-
-# get User class, could not be auth.User
-User = UserSocialAuth._meta.get_field('user').rel.to
-
-
-class SocialAuthBackend(ModelBackend):
- """A django.contrib.auth backend that authenticates the user based on
- a authentication provider response"""
- name = '' # provider name, it's stored in database
-
- def authenticate(self, *args, **kwargs):
- """Authenticate user using social credentials
-
- Authentication is made if this is the correct backend, backend
- verification is made by kwargs inspection for current backend
- name presence.
- """
- # Validate backend and arguments. Require that the OAuth response
- # be passed in as a keyword argument, to make sure we don't match
- # the username/password calling conventions of authenticate.
- if not (self.name and kwargs.get(self.name) and 'response' in kwargs):
- return None
-
- response = kwargs.get('response')
- details = self.get_user_details(response)
- uid = self.get_user_id(details, response)
- try:
- social_user = UserSocialAuth.objects.select_related('user')\
- .get(provider=self.name,
- uid=uid)
- except UserSocialAuth.DoesNotExist:
- user = kwargs.get('user')
- if user is None: # new user
- if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', True):
- return None
- username = self.username(details)
- email = details.get('email')
- user = User.objects.create_user(username=username, email=email)
- social_user = self.associate_auth(user, uid, response, details)
- else:
- user = social_user.user
-
- # Update user account data.
- self.update_user_details(user, response, details)
-
- # Update extra_data storage, unless disabled by setting
- if getattr(settings, 'SOCIAL_AUTH_EXTRA_DATA', True):
- extra_data = self.extra_data(user, uid, response, details)
- if extra_data:
- social_user.extra_data = extra_data
- social_user.save()
-
- return user
-
- def username(self, details):
- """Return an unique username, if SOCIAL_AUTH_FORCE_RANDOM_USERNAME
- setting is True, then username will be a random 30 chars md5 hash
- """
- def get_random_username():
- """Return hash from random string cut at 30 chars"""
- return md5_constructor(urandom(10)).hexdigest()[:30]
-
- if getattr(settings, 'SOCIAL_AUTH_FORCE_RANDOM_USERNAME', False):
- username = get_random_username()
- elif USERNAME in details:
- username = details[USERNAME]
- elif hasattr(settings, 'SOCIAL_AUTH_DEFAULT_USERNAME'):
- username = settings.SOCIAL_AUTH_DEFAULT_USERNAME
- if callable(username):
- username = username()
- else:
- username = get_random_username()
-
- name, idx = username, 2
- while True:
- try:
- User.objects.get(username=name)
- name = username + str(idx)
- idx += 1
- except User.DoesNotExist:
- username = name
- break
- return username
-
- def associate_auth(self, user, uid, response, details):
- """Associate a Social Auth with an user account."""
- return UserSocialAuth.objects.create(user=user, uid=uid,
- provider=self.name)
-
- def extra_data(self, user, uid, response, details):
- """Return default blank user extra data"""
- return ''
-
- def update_user_details(self, user, response, details):
- """Update user details with (maybe) new data. Username is not
- changed if associating a new credential."""
- changed = False # flag to track changes
-
- # check if values update should be left to signals handlers only
- if not getattr(settings, 'SOCIAL_AUTH_CHANGE_SIGNAL_ONLY', False):
- for name, value in details.iteritems():
- # do not update username, it was already generated by
- # self.username(...) and loaded in given instance
- if name != USERNAME and value and value != getattr(user, name,
- value):
- setattr(user, name, value)
- changed = True
-
- # Fire a pre-update signal sending current backend instance,
- # user instance (created or retrieved from database), service
- # response and processed details, signal handlers must return
- # True or False to signal that something has changed. Send method
- # returns a list of tuples with receiver and it's response
- updated = filter(lambda (receiver, response): response,
- pre_update.send(sender=self.__class__, user=user,
- response=response, details=details))
- if changed or updated:
- user.save()
-
- def get_user_id(self, details, response):
- """Must return a unique ID from values returned on details"""
- raise NotImplementedError('Implement in subclass')
-
- def get_user_details(self, response):
- """Must return user details in a know internal struct:
- {USERNAME: <username if any>,
- 'email': <user email if any>,
- 'fullname': <user full name if any>,
- 'first_name': <user first name if any>,
- 'last_name': <user last name if any>}
- """
- raise NotImplementedError('Implement in subclass')
-
- def get_user(self, user_id):
- """Return user instance for @user_id"""
- try:
- return User.objects.get(pk=user_id)
- except User.DoesNotExist:
- return None
-
-
-class OAuthBackend(SocialAuthBackend):
- """OAuth authentication backend base class"""
- def get_user_id(self, details, response):
- "OAuth providers return an unique user id in response"""
- return response['id']
-
- def extra_data(self, user, uid, response, details):
- """Return access_token to store in extra_data field"""
- return response.get('access_token', '')
-
-
-class TwitterBackend(OAuthBackend):
- """Twitter OAuth authentication backend"""
- name = 'twitter'
-
- def get_user_details(self, response):
- """Return user details from Twitter account"""
- return {USERNAME: response['screen_name'],
- 'email': '', # not supplied
- 'fullname': response['name'],
- 'first_name': response['name'],
- 'last_name': ''}
-
-
-class OrkutBackend(OAuthBackend):
- """Orkut OAuth authentication backend"""
- name = 'orkut'
-
- def get_user_details(self, response):
- """Return user details from Orkut account"""
- return {USERNAME: response['displayName'],
- 'email': response['emails'][0]['value'],
- 'fullname': response['displayName'],
- 'firstname': response['name']['givenName'],
- 'lastname': response['name']['familyName']}
-
-
-class GoogleOAuthBackend(OAuthBackend):
- """Google OAuth authentication backend"""
- name = 'google-oauth'
-
- def get_user_id(self, details, response):
- "Use google email as unique id"""
- return details['email']
-
- def get_user_details(self, response):
- """Return user details from Orkut account"""
- email = response['email']
- return {USERNAME: email.split('@', 1)[0],
- 'email': email,
- 'fullname': '',
- 'first_name': '',
- 'last_name': ''}
-
-
-class FacebookBackend(OAuthBackend):
- """Facebook OAuth authentication backend"""
- name = 'facebook'
-
- def get_user_details(self, response):
- """Return user details from Facebook account"""
- return {USERNAME: response['name'],
- 'email': response.get('email', ''),
- 'fullname': response['name'],
- 'first_name': response.get('first_name', ''),
- 'last_name': response.get('last_name', '')}
-
-class OpenIDBackend(SocialAuthBackend):
- """Generic OpenID authentication backend"""
- name = 'openid'
-
- def get_user_id(self, details, response):
- """Return user unique id provided by service"""
- return response.identity_url
-
- def get_user_details(self, response):
- """Return user details from an OpenID request"""
- values = {USERNAME: '', 'email': '', 'fullname': '',
- 'first_name': '', 'last_name': ''}
-
- resp = sreg.SRegResponse.fromSuccessResponse(response)
- if resp:
- values.update((name, resp.get(name) or values.get(name) or '')
- for name in ('email', 'fullname', 'nickname'))
-
- # Use Attribute Exchange attributes if provided
- resp = ax.FetchResponse.fromSuccessResponse(response)
- if resp:
- values.update((alias.replace('old_', ''), resp.getSingle(src))
- for src, alias in OLD_AX_ATTRS + AX_SCHEMA_ATTRS)
-
- fullname = values.get('fullname') or ''
- first_name = values.get('first_name') or ''
- last_name = values.get('last_name') or ''
-
- if not fullname and first_name and last_name:
- fullname = first_name + ' ' + last_name
- elif fullname:
- try: # Try to split name for django user storage
- first_name, last_name = fullname.rsplit(' ', 1)
- except ValueError:
- last_name = fullname
-
- values.update({'fullname': fullname, 'first_name': first_name,
- 'last_name': last_name,
- USERNAME: values.get(USERNAME) or \
- (first_name.title() + last_name.title())})
- return values
-
-
-class GoogleBackend(OpenIDBackend):
- """Google OpenID authentication backend"""
- name = 'google'
-
-
-class YahooBackend(OpenIDBackend):
- """Yahoo OpenID authentication backend"""
- name = 'yahoo'
-
-
-class LiveJournalBackend(OpenIDBackend):
- """LiveJournal OpenID authentication backend"""
- name = 'livejournal'
-
- def get_user_details(self, response):
- """Generate username from identity url"""
- values = super(LiveJournalBackend, self).get_user_details(response)
- if not values.get(USERNAME):
- values[USERNAME] = urlparse.urlsplit(response.identity_url)\
- .netloc.split('.', 1)[0]
- return values
--- /dev/null
+"""
+Base backends structures.
+
+This module defines base classes needed to define custom OpenID or OAuth
+auth services from third parties. This customs must subclass an Auth and
+and Backend class, check current implementation for examples.
+
+Also the modules *must* define a BACKENDS dictionary with the backend name
+(which is used for URLs matching) and Auth class, otherwise it won't be
+enabled.
+"""
+from os import urandom, walk
+from os.path import basename
+from httplib import HTTPSConnection
+
+from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions import sreg, ax
+
+from oauth.oauth import OAuthConsumer, OAuthToken, OAuthRequest, \
+ OAuthSignatureMethod_HMAC_SHA1
+
+from django.conf import settings
+from django.contrib.auth import authenticate
+from django.contrib.auth.backends import ModelBackend
+from django.utils.hashcompat import md5_constructor
+from django.utils.importlib import import_module
+
+from social_auth.models import UserSocialAuth
+from social_auth.store import DjangoOpenIDStore
+from social_auth.signals import pre_update
+
+
+# key for username in user details dict used around, see get_user_details
+# method
+USERNAME = 'username'
+
+# OpenID configuration
+OLD_AX_ATTRS = [
+ ('http://schema.openid.net/contact/email', 'old_email'),
+ ('http://schema.openid.net/namePerson', 'old_fullname'),
+ ('http://schema.openid.net/namePerson/friendly', 'old_nickname')
+]
+AX_SCHEMA_ATTRS = [
+ # Request both the full name and first/last components since some
+ # providers offer one but not the other.
+ ('http://axschema.org/contact/email', 'email'),
+ ('http://axschema.org/namePerson', 'fullname'),
+ ('http://axschema.org/namePerson/first', 'first_name'),
+ ('http://axschema.org/namePerson/last', 'last_name'),
+ ('http://axschema.org/namePerson/friendly', 'nickname'),
+]
+SREG_ATTR = ['email', 'fullname', 'nickname']
+OPENID_ID_FIELD = 'openid_identifier'
+SESSION_NAME = 'openid'
+
+# get User class, could not be auth.User
+User = UserSocialAuth._meta.get_field('user').rel.to
+
+
+class SocialAuthBackend(ModelBackend):
+ """A django.contrib.auth backend that authenticates the user based on
+ a authentication provider response"""
+ name = '' # provider name, it's stored in database
+
+ def authenticate(self, *args, **kwargs):
+ """Authenticate user using social credentials
+
+ Authentication is made if this is the correct backend, backend
+ verification is made by kwargs inspection for current backend
+ name presence.
+ """
+ # Validate backend and arguments. Require that the OAuth response
+ # be passed in as a keyword argument, to make sure we don't match
+ # the username/password calling conventions of authenticate.
+ if not (self.name and kwargs.get(self.name) and 'response' in kwargs):
+ return None
+
+ response = kwargs.get('response')
+ details = self.get_user_details(response)
+ uid = self.get_user_id(details, response)
+ try:
+ social_user = UserSocialAuth.objects.select_related('user')\
+ .get(provider=self.name,
+ uid=uid)
+ except UserSocialAuth.DoesNotExist:
+ user = kwargs.get('user')
+ if user is None: # new user
+ if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', True):
+ return None
+ username = self.username(details)
+ email = details.get('email')
+ user = User.objects.create_user(username=username, email=email)
+ social_user = self.associate_auth(user, uid, response, details)
+ else:
+ user = social_user.user
+
+ # Update user account data.
+ self.update_user_details(user, response, details)
+
+ # Update extra_data storage, unless disabled by setting
+ if getattr(settings, 'SOCIAL_AUTH_EXTRA_DATA', True):
+ extra_data = self.extra_data(user, uid, response, details)
+ if extra_data:
+ social_user.extra_data = extra_data
+ social_user.save()
+
+ return user
+
+ def username(self, details):
+ """Return an unique username, if SOCIAL_AUTH_FORCE_RANDOM_USERNAME
+ setting is True, then username will be a random 30 chars md5 hash
+ """
+ def get_random_username():
+ """Return hash from random string cut at 30 chars"""
+ return md5_constructor(urandom(10)).hexdigest()[:30]
+
+ if getattr(settings, 'SOCIAL_AUTH_FORCE_RANDOM_USERNAME', False):
+ username = get_random_username()
+ elif USERNAME in details:
+ username = details[USERNAME]
+ elif hasattr(settings, 'SOCIAL_AUTH_DEFAULT_USERNAME'):
+ username = settings.SOCIAL_AUTH_DEFAULT_USERNAME
+ if callable(username):
+ username = username()
+ else:
+ username = get_random_username()
+
+ name, idx = username, 2
+ while True:
+ try:
+ User.objects.get(username=name)
+ name = username + str(idx)
+ idx += 1
+ except User.DoesNotExist:
+ username = name
+ break
+ return username
+
+ def associate_auth(self, user, uid, response, details):
+ """Associate a Social Auth with an user account."""
+ return UserSocialAuth.objects.create(user=user, uid=uid,
+ provider=self.name)
+
+ def extra_data(self, user, uid, response, details):
+ """Return default blank user extra data"""
+ return ''
+
+ def update_user_details(self, user, response, details):
+ """Update user details with (maybe) new data. Username is not
+ changed if associating a new credential."""
+ changed = False # flag to track changes
+
+ # check if values update should be left to signals handlers only
+ if not getattr(settings, 'SOCIAL_AUTH_CHANGE_SIGNAL_ONLY', False):
+ for name, value in details.iteritems():
+ # do not update username, it was already generated by
+ # self.username(...) and loaded in given instance
+ if name != USERNAME and value and value != getattr(user, name,
+ value):
+ setattr(user, name, value)
+ changed = True
+
+ # Fire a pre-update signal sending current backend instance,
+ # user instance (created or retrieved from database), service
+ # response and processed details, signal handlers must return
+ # True or False to signal that something has changed. Send method
+ # returns a list of tuples with receiver and it's response
+ updated = filter(lambda (receiver, response): response,
+ pre_update.send(sender=self.__class__, user=user,
+ response=response, details=details))
+ if changed or updated:
+ user.save()
+
+ def get_user_id(self, details, response):
+ """Must return a unique ID from values returned on details"""
+ raise NotImplementedError('Implement in subclass')
+
+ def get_user_details(self, response):
+ """Must return user details in a know internal struct:
+ {USERNAME: <username if any>,
+ 'email': <user email if any>,
+ 'fullname': <user full name if any>,
+ 'first_name': <user first name if any>,
+ 'last_name': <user last name if any>}
+ """
+ raise NotImplementedError('Implement in subclass')
+
+ def get_user(self, user_id):
+ """Return user instance for @user_id"""
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ return None
+
+
+class OAuthBackend(SocialAuthBackend):
+ """OAuth authentication backend base class"""
+ def get_user_id(self, details, response):
+ "OAuth providers return an unique user id in response"""
+ return response['id']
+
+ def extra_data(self, user, uid, response, details):
+ """Return access_token to store in extra_data field"""
+ return response.get('access_token', '')
+
+
+class OpenIDBackend(SocialAuthBackend):
+ """Generic OpenID authentication backend"""
+ name = 'openid'
+
+ def get_user_id(self, details, response):
+ """Return user unique id provided by service"""
+ return response.identity_url
+
+ def get_user_details(self, response):
+ """Return user details from an OpenID request"""
+ values = {USERNAME: '', 'email': '', 'fullname': '',
+ 'first_name': '', 'last_name': ''}
+
+ resp = sreg.SRegResponse.fromSuccessResponse(response)
+ if resp:
+ values.update((name, resp.get(name) or values.get(name) or '')
+ for name in ('email', 'fullname', 'nickname'))
+
+ # Use Attribute Exchange attributes if provided
+ resp = ax.FetchResponse.fromSuccessResponse(response)
+ if resp:
+ values.update((alias.replace('old_', ''), resp.getSingle(src))
+ for src, alias in OLD_AX_ATTRS + AX_SCHEMA_ATTRS)
+
+ fullname = values.get('fullname') or ''
+ first_name = values.get('first_name') or ''
+ last_name = values.get('last_name') or ''
+
+ if not fullname and first_name and last_name:
+ fullname = first_name + ' ' + last_name
+ elif fullname:
+ try: # Try to split name for django user storage
+ first_name, last_name = fullname.rsplit(' ', 1)
+ except ValueError:
+ last_name = fullname
+
+ values.update({'fullname': fullname, 'first_name': first_name,
+ 'last_name': last_name,
+ USERNAME: values.get(USERNAME) or \
+ (first_name.title() + last_name.title())})
+ return values
+
+
+class BaseAuth(object):
+ """Base authentication class, new authenticators should subclass
+ and implement needed methods"""
+ def __init__(self, request, redirect):
+ self.request = request
+ self.redirect = redirect
+
+ def auth_url(self):
+ """Must return redirect URL to auth provider"""
+ raise NotImplementedError('Implement in subclass')
+
+ def auth_html(self):
+ """Must return login HTML content returned by provider"""
+ raise NotImplementedError('Implement in subclass')
+
+ def auth_complete(self, *args, **kwargs):
+ """Completes loging process, must return user instance"""
+ raise NotImplementedError('Implement in subclass')
+
+ @property
+ def uses_redirect(self):
+ """Return True if this provider uses redirect url method,
+ otherwise return false."""
+ return True
+
+
+class OpenIdAuth(BaseAuth):
+ """
+ OpenId process handling
+ @AUTH_BACKEND Authorization backend related with this service
+ """
+ AUTH_BACKEND = OpenIDBackend
+
+ def auth_url(self):
+ openid_request = self.setup_request()
+ # Construct completion URL, including page we should redirect to
+ return_to = self.request.build_absolute_uri(self.redirect)
+ trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
+ self.request.build_absolute_uri('/'))
+ return openid_request.redirectURL(trust_root, return_to)
+
+ def auth_html(self):
+ openid_request = self.setup_request()
+ return_to = self.request.build_absolute_uri(self.redirect)
+ trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
+ self.request.build_absolute_uri('/'))
+ form_tag = {'id': 'openid_message'}
+ return openid_request.htmlMarkup(trust_root, return_to,
+ form_tag_attrs=form_tag)
+
+ def auth_complete(self, *args, **kwargs):
+ response = self.consumer().complete(dict(self.request.REQUEST.items()),
+ self.request.build_absolute_uri())
+ if not response:
+ raise ValueError('This is an OpenID relying party endpoint')
+ elif response.status == SUCCESS:
+ kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
+ return authenticate(*args, **kwargs)
+ elif response.status == FAILURE:
+ raise ValueError('OpenID authentication failed: %s' % \
+ response.message)
+ elif response.status == CANCEL:
+ raise ValueError('Authentication cancelled')
+ else:
+ raise ValueError('Unknown OpenID response type: %r' % \
+ response.status)
+
+ def setup_request(self):
+ """Setup request"""
+ openid_request = self.openid_request()
+ # Request some user details. Use attribute exchange if provider
+ # advertises support.
+ if openid_request.endpoint.supportsType(ax.AXMessage.ns_uri):
+ fetch_request = ax.FetchRequest()
+ # 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))
+ else:
+ fetch_request = sreg.SRegRequest(optional=SREG_ATTR)
+ openid_request.addExtension(fetch_request)
+
+ return openid_request
+
+ def consumer(self):
+ """Create an OpenID Consumer object for the given Django request."""
+ return Consumer(self.request.session.setdefault(SESSION_NAME, {}),
+ DjangoOpenIDStore())
+
+ @property
+ def uses_redirect(self):
+ """Return true if openid request will be handled with redirect or
+ HTML content will be returned.
+ """
+ if not hasattr(self, '_uses_redirect'):
+ setattr(self, '_uses_redirect',
+ self.openid_request().shouldSendRedirect())
+ return getattr(self, '_uses_redirect', True)
+
+ def openid_request(self):
+ """Return openid request"""
+ if not hasattr(self, '_openid_request'):
+ openid_url = self.openid_url()
+ try:
+ openid_request = self.consumer().begin(openid_url)
+ except DiscoveryFailure, err:
+ raise ValueError('OpenID discovery error: %s' % err)
+ else:
+ setattr(self, '_openid_request', openid_request)
+ return getattr(self, '_openid_request', None)
+
+ def openid_url(self):
+ """Return service provider URL.
+ This base class is generic accepting a POST parameter that specifies
+ provider URL."""
+ if self.request.method != 'POST' or \
+ OPENID_ID_FIELD not in self.request.POST:
+ raise ValueError('Missing openid identifier')
+ return self.request.POST[OPENID_ID_FIELD]
+
+
+class BaseOAuth(BaseAuth):
+ """OAuth base class"""
+ def __init__(self, request, redirect):
+ """Init method"""
+ super(BaseOAuth, self).__init__(request, redirect)
+ self.redirect_uri = self.request.build_absolute_uri(self.redirect)
+
+
+class ConsumerBasedOAuth(BaseOAuth):
+ """Consumer based mechanism OAuth authentication, fill the needed
+ parameters to communicate properly with authentication service.
+
+ @AUTHORIZATION_URL Authorization service url
+ @REQUEST_TOKEN_URL Request token URL
+ @ACCESS_TOKEN_URL Access token URL
+ @SERVER_URL Authorization server URL
+ @AUTH_BACKEND Authorization backend related with
+ this service
+ """
+ AUTHORIZATION_URL = ''
+ REQUEST_TOKEN_URL = ''
+ ACCESS_TOKEN_URL = ''
+ SERVER_URL = ''
+ AUTH_BACKEND = None
+
+ def auth_url(self):
+ """Returns redirect url"""
+ token = self.unauthorized_token()
+ name = self.AUTH_BACKEND.name + 'unauthorized_token_name'
+ self.request.session[name] = token.to_string()
+ return self.oauth_request(token, self.AUTHORIZATION_URL).to_url()
+
+ def auth_complete(self, *args, **kwargs):
+ """Returns user, might be logged in"""
+ name = self.AUTH_BACKEND.name + 'unauthorized_token_name'
+ unauthed_token = self.request.session.get(name)
+ if not unauthed_token:
+ raise ValueError('Missing unauthorized token')
+
+ token = OAuthToken.from_string(unauthed_token)
+ if token.key != self.request.GET.get('oauth_token', 'no-token'):
+ raise ValueError('Incorrect tokens')
+
+ access_token = self.access_token(token)
+ data = self.user_data(access_token)
+ if data is not None:
+ data['access_token'] = access_token.to_string()
+
+ kwargs.update({'response': data, self.AUTH_BACKEND.name: True})
+ return authenticate(*args, **kwargs)
+
+ def unauthorized_token(self):
+ """Return request for unauthorized token (first stage)"""
+ request = self.oauth_request(token=None, url=self.REQUEST_TOKEN_URL)
+ response = self.fetch_response(request)
+ return OAuthToken.from_string(response)
+
+ def oauth_request(self, token, url, extra_params=None):
+ """Generate OAuth request, setups callback url"""
+ params = {'oauth_callback': self.redirect_uri}
+ if extra_params:
+ params.update(extra_params)
+
+ if 'oauth_verifier' in self.request.GET:
+ params['oauth_verifier'] = self.request.GET['oauth_verifier']
+ request = OAuthRequest.from_consumer_and_token(self.consumer,
+ token=token,
+ http_url=url,
+ parameters=params)
+ request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer,
+ token)
+ return request
+
+ def fetch_response(self, request):
+ """Executes request and fetchs service response"""
+ self.connection.request(request.http_method, request.to_url())
+ response = self.connection.getresponse()
+ return response.read()
+
+ def access_token(self, token):
+ """Return request for access token value"""
+ request = self.oauth_request(token, self.ACCESS_TOKEN_URL)
+ return OAuthToken.from_string(self.fetch_response(request))
+
+ def user_data(self, access_token):
+ """Loads user data from service"""
+ raise NotImplementedError('Implement in subclass')
+
+ @property
+ def connection(self):
+ """Setups connection"""
+ conn = getattr(self, '_connection', None)
+ if conn is None:
+ conn = HTTPSConnection(self.SERVER_URL)
+ setattr(self, '_connection', conn)
+ return conn
+
+ @property
+ def consumer(self):
+ """Setups consumer"""
+ cons = getattr(self, '_consumer', None)
+ if cons is None:
+ cons = OAuthConsumer(*self.get_key_and_secret())
+ setattr(self, '_consumer', cons)
+ return cons
+
+ 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.
+ """
+ raise NotImplementedError('Implement in subclass')
+
+
+# import sources from where check for auth backends
+SOCIAL_AUTH_IMPORT_SOURCES = (
+ 'social_auth.backends',
+ 'social_auth.backends.contrib',
+) + getattr(settings, 'SOCIAL_AUTH_IMPORT_BACKENDS', ())
+
+def get_backends():
+ backends = {}
+
+ for mod_name in SOCIAL_AUTH_IMPORT_SOURCES:
+ try:
+ mod = import_module(mod_name)
+ except ImportError:
+ continue
+
+ for directory, subdir, files in walk(mod.__path__[0]):
+ for name in filter(lambda name: name.endswith('.py'), files):
+ try:
+ name = basename(name).replace('.py', '')
+ sub = import_module(mod_name + '.' + name)
+ backends.update(sub.BACKENDS)
+ except (ImportError, AttributeError):
+ pass
+ return backends
+
+# load backends from defined modules
+BACKENDS = get_backends()
+
+def get_backend(name, *args, **kwargs):
+ """Return auth backend instance *if* it's registered, None in other case"""
+ return BACKENDS.get(name, lambda *args, **kwargs: None)(*args, **kwargs)
--- /dev/null
+"""Contrib auth modules"""
--- /dev/null
+"""
+LiveJournal OpenID support.
+
+This contribution adds support for LiveJournal OpenID service in the form
+username.livejournal.com. Username is retrieved from the identity url.
+"""
+import urlparse
+
+from social_auth.backends import OpenIDBackend, OpenIdAuth, USERNAME
+
+
+# LiveJournal conf
+LIVEJOURNAL_URL = 'http://%s.livejournal.com'
+LIVEJOURNAL_USER_FIELD = 'openid_lj_user'
+
+
+class LiveJournalBackend(OpenIDBackend):
+ """LiveJournal OpenID authentication backend"""
+ name = 'livejournal'
+
+ def get_user_details(self, response):
+ """Generate username from identity url"""
+ values = super(LiveJournalBackend, self).get_user_details(response)
+ values[USERNAME] = values.get(USERNAME) or \
+ urlparse.urlsplit(response.identity_url)\
+ .netloc.split('.', 1)[0]
+ return values
+
+
+class LiveJournalAuth(OpenIdAuth):
+ """LiveJournal OpenID authentication"""
+ AUTH_BACKEND = LiveJournalBackend
+
+ def uses_redirect(self):
+ """LiveJournal uses redirect"""
+ return True
+
+ def openid_url(self):
+ """Returns LiveJournal authentication URL"""
+ if self.request.method != 'POST' or \
+ not self.request.POST.get(LIVEJOURNAL_USER_FIELD):
+ raise ValueError, 'Missing LiveJournal user identifier'
+ return LIVEJOURNAL_URL % self.request.POST[LIVEJOURNAL_USER_FIELD]
+
+
+# Backend definition
+BACKENDS = {
+ 'livejournal': LiveJournalAuth,
+}
--- /dev/null
+"""
+Orkut OAuth support.
+
+This contribution adds support for Orkut OAuth service. The scope is
+limited to http://orkut.gmodules.com/social/ by default, but can be
+extended with ORKUT_EXTRA_SCOPE on project settings. Also name, display
+name and emails are the default requested user data, but extra values
+can be specified by defining ORKUT_EXTRA_DATA setting.
+
+OAuth settings ORKUT_CONSUMER_KEY and ORKUT_CONSUMER_SECRET are needed
+to enable this service support.
+"""
+import urllib
+
+from django.conf import settings
+from django.utils import simplejson
+
+from social_auth.backends import OAuthBackend, USERNAME
+from social_auth.backends.google import BaseGoogleOAuth
+
+
+# Orkut configuration
+# default scope, specify extra scope in settings as in:
+# ORKUT_EXTRA_SCOPE = ['...']
+ORKUT_SCOPE = ['http://orkut.gmodules.com/social/']
+ORKUT_REST_ENDPOINT = 'http://www.orkut.com/social/rpc'
+ORKUT_DEFAULT_DATA = 'name,displayName,emails'
+
+
+class OrkutBackend(OAuthBackend):
+ """Orkut OAuth authentication backend"""
+ name = 'orkut'
+
+ def get_user_details(self, response):
+ """Return user details from Orkut account"""
+ return {USERNAME: response['displayName'],
+ 'email': response['emails'][0]['value'],
+ 'fullname': response['displayName'],
+ 'firstname': response['name']['givenName'],
+ 'lastname': response['name']['familyName']}
+
+
+class OrkutAuth(BaseGoogleOAuth):
+ """Orkut OAuth authentication mechanism"""
+ AUTH_BACKEND = OrkutBackend
+
+ def user_data(self, access_token):
+ """Loads user data from Orkut service"""
+ fields = ORKUT_DEFAULT_DATA
+ if hasattr(settings, 'ORKUT_EXTRA_DATA'):
+ fields += ',' + settings.ORKUT_EXTRA_DATA
+ scope = ORKUT_SCOPE + getattr(settings, 'ORKUT_EXTRA_SCOPE', [])
+ params = {'method': 'people.get',
+ 'id': 'myself',
+ 'userId': '@me',
+ 'groupId': '@self',
+ 'fields': fields,
+ 'scope': ' '.join(scope)}
+ request = self.oauth_request(access_token, ORKUT_REST_ENDPOINT, params)
+ response = urllib.urlopen(request.to_url()).read()
+ try:
+ return simplejson.loads(response)['data']
+ except (simplejson.JSONDecodeError, KeyError):
+ return None
+
+ def get_key_and_secret(self):
+ """Return Orkut Consumer Key and Consumer Secret pair"""
+ return settings.ORKUT_CONSUMER_KEY, settings.ORKUT_CONSUMER_SECRET
+
+
+# Backend definition
+BACKENDS = {
+ 'orkut': OrkutAuth,
+}
--- /dev/null
+"""
+Facebook OAuth support.
+
+This contribution adds support for Facebook OAuth service. The settings
+FACEBOOK_APP_ID and FACEBOOK_API_SECRET must be defined with the values
+given by Facebook application registration process.
+
+Extended permissions are supported by defining FACEBOOK_EXTENDED_PERMISSIONS
+setting, it must be a list of values to request.
+"""
+import cgi
+import urllib
+
+from django.conf import settings
+from django.utils import simplejson
+from django.contrib.auth import authenticate
+
+from social_auth.backends import BaseOAuth, OAuthBackend, USERNAME
+
+
+# Facebook configuration
+FACEBOOK_SERVER = 'graph.facebook.com'
+FACEBOOK_AUTHORIZATION_URL = 'https://%s/oauth/authorize' % FACEBOOK_SERVER
+FACEBOOK_ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % FACEBOOK_SERVER
+FACEBOOK_CHECK_AUTH = 'https://%s/me' % FACEBOOK_SERVER
+
+
+class FacebookBackend(OAuthBackend):
+ """Facebook OAuth authentication backend"""
+ name = 'facebook'
+
+ def get_user_details(self, response):
+ """Return user details from Facebook account"""
+ return {USERNAME: response['name'],
+ 'email': response.get('email', ''),
+ 'fullname': response['name'],
+ 'first_name': response.get('first_name', ''),
+ 'last_name': response.get('last_name', '')}
+
+
+class FacebookAuth(BaseOAuth):
+ """Facebook OAuth mechanism"""
+ def auth_url(self):
+ """Returns redirect url"""
+ args = {'client_id': settings.FACEBOOK_APP_ID,
+ 'redirect_uri': self.redirect_uri}
+ if hasattr(settings, 'FACEBOOK_EXTENDED_PERMISSIONS'):
+ args['scope'] = ','.join(settings.FACEBOOK_EXTENDED_PERMISSIONS)
+ return FACEBOOK_AUTHORIZATION_URL + '?' + urllib.urlencode(args)
+
+ def auth_complete(self, *args, **kwargs):
+ """Returns user, might be logged in"""
+ if 'code' in self.request.GET:
+ url = FACEBOOK_ACCESS_TOKEN_URL + '?' + \
+ urllib.urlencode({'client_id': settings.FACEBOOK_APP_ID,
+ 'redirect_uri': self.redirect_uri,
+ 'client_secret': settings.FACEBOOK_API_SECRET,
+ 'code': self.request.GET['code']})
+ response = cgi.parse_qs(urllib.urlopen(url).read())
+
+ access_token = response['access_token'][0]
+ data = self.user_data(access_token)
+ if data is not None:
+ if 'error' in data:
+ raise ValueError('Authentication error')
+ data['access_token'] = access_token
+
+ kwargs.update({'response': data, FacebookBackend.name: True})
+ return authenticate(*args, **kwargs)
+ else:
+ raise ValueError('Authentication error')
+
+ def user_data(self, access_token):
+ """Loads user data from service"""
+ params = {'access_token': access_token}
+ url = FACEBOOK_CHECK_AUTH + '?' + urllib.urlencode(params)
+ try:
+ return simplejson.load(urllib.urlopen(url))
+ except simplejson.JSONDecodeError:
+ return None
+
+
+# Backend definition
+BACKENDS = {
+ 'facebook': FacebookAuth,
+}
--- /dev/null
+"""
+Google OpenID and OAuth support
+
+OAuth works straightforward using anonymous configurations, username
+is generated by requesting email to the not documented, googleapis.com
+service. Registered applications can define settings GOOGLE_CONSUMER_KEY
+and GOOGLE_CONSUMER_SECRET and they will be used in the auth process.
+Setting GOOGLE_OAUTH_EXTRA_SCOPE can be used to access different user
+related data, like calendar, contacts, docs, etc.
+
+OpenID also works straightforward, it doesn't need further configurations.
+"""
+from urllib2 import Request, urlopen
+
+from django.conf import settings
+from django.utils import simplejson
+
+from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, \
+ OAuthBackend, OpenIDBackend, USERNAME
+
+
+# Google OAuth base configuration
+GOOGLE_SERVER = 'www.google.com'
+GOOGLE_REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
+GOOGLE_ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
+GOOGLE_AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
+# scope for user email, specify extra scopes in settings, for example:
+# GOOGLE_OAUTH_EXTRA_SCOPE = ['https://www.google.com/m8/feeds/']
+GOOGLE_OAUTH_SCOPE = ['https://www.googleapis.com/auth/userinfo#email']
+GOOGLEAPIS_EMAIL = 'https://www.googleapis.com/userinfo/email'
+GOOGLE_OPENID_URL = 'https://www.google.com/accounts/o8/id'
+
+
+# Backends
+class GoogleOAuthBackend(OAuthBackend):
+ """Google OAuth authentication backend"""
+ name = 'google-oauth'
+
+ def get_user_id(self, details, response):
+ "Use google email as unique id"""
+ return details['email']
+
+ def get_user_details(self, response):
+ """Return user details from Orkut account"""
+ email = response['email']
+ return {USERNAME: email.split('@', 1)[0],
+ 'email': email,
+ 'fullname': '',
+ 'first_name': '',
+ 'last_name': ''}
+
+
+class GoogleBackend(OpenIDBackend):
+ """Google OpenID authentication backend"""
+ name = 'google'
+
+
+# Auth classes
+class GoogleAuth(OpenIdAuth):
+ """Google OpenID authentication"""
+ AUTH_BACKEND = GoogleBackend
+
+ def openid_url(self):
+ """Return Google OpenID service url"""
+ return GOOGLE_OPENID_URL
+
+
+class BaseGoogleOAuth(ConsumerBasedOAuth):
+ """Base class for Google OAuth mechanism"""
+ AUTHORIZATION_URL = GOOGLE_AUTHORIZATION_URL
+ REQUEST_TOKEN_URL = GOOGLE_REQUEST_TOKEN_URL
+ ACCESS_TOKEN_URL = GOOGLE_ACCESS_TOKEN_URL
+ SERVER_URL = GOOGLE_SERVER
+ AUTH_BACKEND = None
+
+ def user_data(self, access_token):
+ """Loads user data from G service"""
+ raise NotImplementedError('Implement in subclass')
+
+ def get_key_and_secret(self):
+ """Return Consumer Key and Consumer Secret pair"""
+ raise NotImplementedError('Implement in subclass')
+
+
+class GoogleOAuth(BaseGoogleOAuth):
+ """Google OAuth authorization mechanism"""
+ AUTH_BACKEND = GoogleOAuthBackend
+
+ def user_data(self, access_token):
+ """Loads user data data from googleapis service, only email so far
+ as it's described in:
+ http://sites.google.com/site/oauthgoog/Home/emaildisplayscope
+ OAuth parameters needs to be passed in the queryset and
+ Authorization header, this behavior is listed in:
+ http://groups.google.com/group/oauth/browse_thread/thread/d15add9beb418ebc
+ """
+ url = self.oauth_request(access_token, GOOGLEAPIS_EMAIL,
+ {'alt': 'json'}).to_url()
+ params = url.split('?', 1)[1]
+ request = Request(url)
+ request.headers['Authorization'] = params # setup header
+ response = urlopen(request).read()
+ try:
+ return simplejson.loads(response)['data']
+ except (simplejson.JSONDecodeError, KeyError):
+ return None
+
+ def oauth_request(self, token, url, extra_params=None):
+ extra_params = extra_params or {}
+ scope = GOOGLE_OAUTH_SCOPE + \
+ getattr(settings, 'GOOGLE_OAUTH_EXTRA_SCOPE', [])
+ extra_params.update({
+ 'scope': ' '.join(scope),
+ 'xoauth_displayname': getattr(settings, 'GOOGLE_DISPLAY_NAME',
+ 'Social Auth')
+ })
+ return super(GoogleOAuth, self).oauth_request(token, url, extra_params)
+
+ def get_key_and_secret(self):
+ """Return Google OAuth Consumer Key and Consumer Secret pair, uses
+ anonymous by default, beware that this marks the application as not
+ registered and a security badge is displayed on authorization page.
+ http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth
+ """
+ return getattr(settings, 'GOOGLE_CONSUMER_KEY', 'anonymous'), \
+ getattr(settings, 'GOOGLE_CONSUMER_SECRET', 'anonymous')
+
+
+# Backend definition
+BACKENDS = {
+ 'google': GoogleAuth,
+ 'google-oauth': GoogleOAuth,
+}
--- /dev/null
+"""
+Twitter OAuth support.
+
+This adds support for Twitter OAuth service. An application must
+be registered first on twitter and the settings TWITTER_CONSUMER_KEY
+and TWITTER_CONSUMER_SECRET must be defined with they corresponding
+values.
+
+User screen name is used to generate username.
+"""
+from django.conf import settings
+from django.utils import simplejson
+
+from social_auth.backends import ConsumerBasedOAuth, OAuthBackend, USERNAME
+
+
+# Twitter configuration
+TWITTER_SERVER = 'api.twitter.com'
+TWITTER_REQUEST_TOKEN_URL = 'https://%s/oauth/request_token' % TWITTER_SERVER
+TWITTER_ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % TWITTER_SERVER
+# Note: oauth/authorize forces the user to authorize every time.
+# oauth/authenticate uses their previous selection, barring revocation.
+TWITTER_AUTHORIZATION_URL = 'http://%s/oauth/authenticate' % TWITTER_SERVER
+TWITTER_CHECK_AUTH = 'https://twitter.com/account/verify_credentials.json'
+
+
+class TwitterBackend(OAuthBackend):
+ """Twitter OAuth authentication backend"""
+ name = 'twitter'
+
+ def get_user_details(self, response):
+ """Return user details from Twitter account"""
+ return {USERNAME: response['screen_name'],
+ 'email': '', # not supplied
+ 'fullname': response['name'],
+ 'first_name': response['name'],
+ 'last_name': ''}
+
+
+class TwitterAuth(ConsumerBasedOAuth):
+ """Twitter OAuth authentication mechanism"""
+ AUTHORIZATION_URL = TWITTER_AUTHORIZATION_URL
+ REQUEST_TOKEN_URL = TWITTER_REQUEST_TOKEN_URL
+ ACCESS_TOKEN_URL = TWITTER_ACCESS_TOKEN_URL
+ SERVER_URL = TWITTER_SERVER
+ AUTH_BACKEND = TwitterBackend
+
+ def user_data(self, access_token):
+ """Return user data provided"""
+ request = self.oauth_request(access_token, TWITTER_CHECK_AUTH)
+ json = self.fetch_response(request)
+ try:
+ return simplejson.loads(json)
+ except simplejson.JSONDecodeError:
+ return None
+
+ def get_key_and_secret(self):
+ """Return Twitter Consumer Key and Consumer Secret pair"""
+ return settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET
+
+
+# Backend definition
+BACKENDS = {
+ 'twitter': TwitterAuth,
+}
--- /dev/null
+"""
+Yahoo OpenID support
+
+No extra configurations are needed to make this work.
+"""
+from social_auth.backends import OpenIDBackend, OpenIdAuth
+
+
+YAHOO_OPENID_URL = 'http://yahoo.com'
+
+
+class YahooBackend(OpenIDBackend):
+ """Yahoo OpenID authentication backend"""
+ name = 'yahoo'
+
+
+class YahooAuth(OpenIdAuth):
+ """Yahoo OpenID authentication"""
+ AUTH_BACKEND = YahooBackend
+
+ def openid_url(self):
+ """Return Yahoo OpenID service url"""
+ return YAHOO_OPENID_URL
+
+
+# Backend definition
+BACKENDS = {
+ 'yahoo': YahooAuth,
+}
+++ /dev/null
-"""Conf settings"""
-# Twitter configuration
-TWITTER_SERVER = 'api.twitter.com'
-TWITTER_REQUEST_TOKEN_URL = 'https://%s/oauth/request_token' % TWITTER_SERVER
-TWITTER_ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % TWITTER_SERVER
-# Note: oauth/authorize forces the user to authorize every time.
-# oauth/authenticate uses their previous selection, barring revocation.
-TWITTER_AUTHORIZATION_URL = 'http://%s/oauth/authenticate' % TWITTER_SERVER
-TWITTER_CHECK_AUTH = 'https://twitter.com/account/verify_credentials.json'
-
-# Facebook configuration
-FACEBOOK_SERVER = 'graph.facebook.com'
-FACEBOOK_AUTHORIZATION_URL = 'https://%s/oauth/authorize' % FACEBOOK_SERVER
-FACEBOOK_ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % FACEBOOK_SERVER
-FACEBOOK_CHECK_AUTH = 'https://%s/me' % FACEBOOK_SERVER
-
-# Google OAuth base configuration
-GOOGLE_SERVER = 'www.google.com'
-GOOGLE_REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
-GOOGLE_ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
-GOOGLE_AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
-# scope for user email, specify extra scopes in settings, for example:
-# GOOGLE_OAUTH_EXTRA_SCOPE = ['https://www.google.com/m8/feeds/']
-GOOGLE_OAUTH_SCOPE = ['https://www.googleapis.com/auth/userinfo#email']
-GOOGLEAPIS_EMAIL = 'https://www.googleapis.com/userinfo/email'
-
-# Orkut configuration
-# default scope, specify extra scope in settings as in:
-# ORKUT_EXTRA_SCOPE = ['...']
-ORKUT_SCOPE = ['http://orkut.gmodules.com/social/']
-ORKUT_REST_ENDPOINT = 'http://www.orkut.com/social/rpc'
-ORKUT_DEFAULT_DATA = 'name,displayName,emails'
-
-# OpenID configuration
-OLD_AX_ATTRS = [
- ('http://schema.openid.net/contact/email', 'old_email'),
- ('http://schema.openid.net/namePerson', 'old_fullname'),
- ('http://schema.openid.net/namePerson/friendly', 'old_nickname')
-]
-AX_SCHEMA_ATTRS = [
- # Request both the full name and first/last components since some
- # providers offer one but not the other.
- ('http://axschema.org/contact/email', 'email'),
- ('http://axschema.org/namePerson', 'fullname'),
- ('http://axschema.org/namePerson/first', 'first_name'),
- ('http://axschema.org/namePerson/last', 'last_name'),
- ('http://axschema.org/namePerson/friendly', 'nickname'),
-]
-AX_ATTRS = AX_SCHEMA_ATTRS + OLD_AX_ATTRS
-SREG_ATTR = ['email', 'fullname', 'nickname']
-OPENID_ID_FIELD = 'openid_identifier'
-SESSION_NAME = 'openid'
-OPENID_GOOGLE_URL = 'https://www.google.com/accounts/o8/id'
-OPENID_YAHOO_URL = 'http://yahoo.com'
-OPENID_LIVEJOURNAL_URL = 'http://%s.livejournal.com'
-OPENID_LIVEJOURNAL_USER_FIELD = 'openid_lj_user'
from openid.store.interface import OpenIDStore
from openid.store.nonce import SKEW
-from .models import Association, Nonce
+from social_auth.models import Association, Nonce
class DjangoOpenIDStore(OpenIDStore):
"""URLs module"""
from django.conf.urls.defaults import patterns, url
-from .views import auth, complete, associate, associate_complete
+from social_auth.views import auth, complete, associate, associate_complete
urlpatterns = patterns('',
from django.contrib.auth import login, REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
-from .auth import get_backend
+from social_auth.backends import get_backend
def auth(request, backend):