From 7fba9e3414d32d48cd4e782caa958cd007362f90 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Mat=C3=ADas=20Aguirre?= Date: Tue, 18 Jan 2011 15:14:54 -0200 Subject: [PATCH] Added backends module with contribs section and simpler way to add extra backends support. Closes gh-16 --- README.rst | 48 +- example/settings.py | 12 +- example/templates/done.html | 12 +- example/templates/home.html | 8 +- social_auth/__init__.py | 2 +- social_auth/admin.py | 2 +- social_auth/auth.py | 471 ------------------ social_auth/backends.py | 289 ----------- social_auth/backends/__init__.py | 515 ++++++++++++++++++++ social_auth/backends/contrib/__init__.py | 1 + social_auth/backends/contrib/livejournal.py | 49 ++ social_auth/backends/contrib/orkut.py | 74 +++ social_auth/backends/facebook.py | 86 ++++ social_auth/backends/google.py | 133 +++++ social_auth/backends/twitter.py | 65 +++ social_auth/backends/yahoo.py | 29 ++ social_auth/conf.py | 56 --- social_auth/store.py | 2 +- social_auth/urls.py | 2 +- social_auth/views.py | 2 +- 20 files changed, 1012 insertions(+), 846 deletions(-) delete mode 100644 social_auth/auth.py delete mode 100644 social_auth/backends.py create mode 100644 social_auth/backends/__init__.py create mode 100644 social_auth/backends/contrib/__init__.py create mode 100644 social_auth/backends/contrib/livejournal.py create mode 100644 social_auth/backends/contrib/orkut.py create mode 100644 social_auth/backends/facebook.py create mode 100644 social_auth/backends/google.py create mode 100644 social_auth/backends/twitter.py create mode 100644 social_auth/backends/yahoo.py delete mode 100644 social_auth/conf.py diff --git a/README.rst b/README.rst index 6f06600..b55d941 100644 --- a/README.rst +++ b/README.rst @@ -29,10 +29,13 @@ credentials, some features are: * `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 @@ -95,26 +98,43 @@ Configuration - 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 = '' diff --git a/example/settings.py b/example/settings.py index 8ea7770..f2e7d7c 100644 --- a/example/settings.py +++ b/example/settings.py @@ -62,13 +62,13 @@ INSTALLED_APPS = ( ) 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', ) diff --git a/example/templates/done.html b/example/templates/done.html index a558549..3b90b81 100644 --- a/example/templates/done.html +++ b/example/templates/done.html @@ -45,7 +45,17 @@ Yahoo {% if yahoo %}(associated){% endif %} - +
  • +
    {% csrf_token %} +
    + + + +
    +
    +
  • {% csrf_token %}
    diff --git a/example/templates/home.html b/example/templates/home.html index f54d252..636d278 100644 --- a/example/templates/home.html +++ b/example/templates/home.html @@ -18,13 +18,13 @@
    • Google
    • Yahoo
    • -
    • - {% csrf_token %} -
      +
    • + {% csrf_token %} +
      -
      +
  • diff --git a/social_auth/__init__.py b/social_auth/__init__.py index bcf6328..13d2ab1 100644 --- a/social_auth/__init__.py +++ b/social_auth/__init__.py @@ -2,5 +2,5 @@ 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)) diff --git a/social_auth/admin.py b/social_auth/admin.py index d8dde01..cd1a60a 100644 --- a/social_auth/admin.py +++ b/social_auth/admin.py @@ -1,7 +1,7 @@ """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): diff --git a/social_auth/auth.py b/social_auth/auth.py deleted file mode 100644 index 0515a6e..0000000 --- a/social_auth/auth.py +++ /dev/null @@ -1,471 +0,0 @@ -"""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) diff --git a/social_auth/backends.py b/social_auth/backends.py deleted file mode 100644 index e98505d..0000000 --- a/social_auth/backends.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -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: , - 'email': , - 'fullname': , - 'first_name': , - 'last_name': } - """ - 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 diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py new file mode 100644 index 0000000..813e79d --- /dev/null +++ b/social_auth/backends/__init__.py @@ -0,0 +1,515 @@ +""" +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: , + 'email': , + 'fullname': , + 'first_name': , + 'last_name': } + """ + 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) diff --git a/social_auth/backends/contrib/__init__.py b/social_auth/backends/contrib/__init__.py new file mode 100644 index 0000000..914e3c1 --- /dev/null +++ b/social_auth/backends/contrib/__init__.py @@ -0,0 +1 @@ +"""Contrib auth modules""" diff --git a/social_auth/backends/contrib/livejournal.py b/social_auth/backends/contrib/livejournal.py new file mode 100644 index 0000000..1b1b18d --- /dev/null +++ b/social_auth/backends/contrib/livejournal.py @@ -0,0 +1,49 @@ +""" +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, +} diff --git a/social_auth/backends/contrib/orkut.py b/social_auth/backends/contrib/orkut.py new file mode 100644 index 0000000..9356063 --- /dev/null +++ b/social_auth/backends/contrib/orkut.py @@ -0,0 +1,74 @@ +""" +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, +} diff --git a/social_auth/backends/facebook.py b/social_auth/backends/facebook.py new file mode 100644 index 0000000..eff17d6 --- /dev/null +++ b/social_auth/backends/facebook.py @@ -0,0 +1,86 @@ +""" +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, +} diff --git a/social_auth/backends/google.py b/social_auth/backends/google.py new file mode 100644 index 0000000..c2a1249 --- /dev/null +++ b/social_auth/backends/google.py @@ -0,0 +1,133 @@ +""" +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, +} diff --git a/social_auth/backends/twitter.py b/social_auth/backends/twitter.py new file mode 100644 index 0000000..04c47e3 --- /dev/null +++ b/social_auth/backends/twitter.py @@ -0,0 +1,65 @@ +""" +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, +} diff --git a/social_auth/backends/yahoo.py b/social_auth/backends/yahoo.py new file mode 100644 index 0000000..e9362f4 --- /dev/null +++ b/social_auth/backends/yahoo.py @@ -0,0 +1,29 @@ +""" +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, +} diff --git a/social_auth/conf.py b/social_auth/conf.py deleted file mode 100644 index 3fcee41..0000000 --- a/social_auth/conf.py +++ /dev/null @@ -1,56 +0,0 @@ -"""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' diff --git a/social_auth/store.py b/social_auth/store.py index ab7ebb4..a1be029 100644 --- a/social_auth/store.py +++ b/social_auth/store.py @@ -6,7 +6,7 @@ from openid.association import Association as OIDAssociation 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): diff --git a/social_auth/urls.py b/social_auth/urls.py index 2185499..b9f19f8 100644 --- a/social_auth/urls.py +++ b/social_auth/urls.py @@ -1,7 +1,7 @@ """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('', diff --git a/social_auth/views.py b/social_auth/views.py index 248d6fa..ed35db7 100644 --- a/social_auth/views.py +++ b/social_auth/views.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse 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): -- 2.39.5