From 86c4d5d056c7e8d8299a381b3509f4f6747f3366 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 8 Nov 2010 08:08:13 -0200 Subject: [PATCH] Commiting missing files --- .gitignore | 1 + README.rst | 50 ++++++++- example/settings.py | 7 +- example/urls.py | 17 ++-- social_auth/auth.py | 218 ++++++++++++++++++++++++++++++++++++++++ social_auth/backends.py | 33 +++--- social_auth/conf.py | 20 ++-- social_auth/models.py | 1 + social_auth/urls.py | 7 +- social_auth/views.py | 45 +-------- 10 files changed, 313 insertions(+), 86 deletions(-) create mode 100644 social_auth/auth.py diff --git a/.gitignore b/.gitignore index 5e2a46f..f62485b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .*.sw[po] +test.db local_settings.py diff --git a/README.rst b/README.rst index 289c419..e396432 100644 --- a/README.rst +++ b/README.rst @@ -3,4 +3,52 @@ Django Social Auth 1. Description -------------- -Basically this is a take +Basically this is a code merge between: + * Django-Socialauth (https://github.com/uswaretech/Django-Socialauth) + * django-twitter-oauth (https://github.com/henriklied/django-twitter-oauth) +the app supplies two views that controls OAuth and OpenID loggin/registration. + +2. OAuth +----------- +Twitter and Facebook Oauth mechanims for authentication/registration, +corresponding apps and settings must be filled: + + - Twitter + * Register a new app at http://twitter.com/apps/new + * Be sure to mark the "Yes, use Twitter for login" checkbox + * Fill "Cosumer Key" and "Consumer Secret" values in settings + TWITTER_CONSUMER_KEY + TWITTER_CONSUMER_SECRET + Twitter demands a redirect url configuration and will force the user + to that address when redirecting, I suggest to setup something like + http://myvirtualapp.com and then configuring myvirtualapp.com in + /etc/hosts file, the port will be missing but works well for testing. + + - Facebook + * Register a new app at http://developers.facebook.com/setup/ + * Fill "App Id" and "App Secret" values in settings: + FACEBOOK_APP_ID + FACEBOOK_API_SECRET + +3. OpenId +--------- +Yahoo and Google OpenId providers are supported, also custom providers +like myopenid.com are supported if provider url is specified by a POST +parameter (openid_identifier). + +4. Bugs +------- +Several, maybe, please report :-) + +5. Copyrights +------------- +Base work is copyrighted by: + +Oauth base code: + Original Copyright goes to Henrik Lied (henriklied) + Code borrowed from https://github.com/henriklied/django-twitter-oauth + +OpenId base code: + django-openid-auth - OpenID integration for django.contrib.auth + Copyright (C) 2007 Simon Willison + Copyright (C) 2008-2010 Canonical Ltd. diff --git a/example/settings.py b/example/settings.py index 68f62f1..69cbe16 100644 --- a/example/settings.py +++ b/example/settings.py @@ -87,14 +87,13 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', + 'django.contrib.admin', 'social_auth', ) AUTHENTICATION_BACKENDS = ( - 'social_auth.backends.TwitterOAuthBackend', - 'social_auth.backends.FacebookOAuthBackend', + 'social_auth.backends.TwitterBackend', + 'social_auth.backends.FacebookBackend', 'social_auth.backends.OpenIDBackend', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/example/urls.py b/example/urls.py index 610e798..150a676 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,12 +1,15 @@ from django.conf.urls.defaults import * +from django.contrib import admin -# Uncomment the next two lines to enable the admin: -# from django.contrib import admin -# admin.autodiscover() +from app.views import home, done, logout -urlpatterns = patterns('', - (r'^', include('social_auth.urls', namespace='social')), - # Uncomment the next line to enable the admin: - # (r'^admin/', include(admin.site.urls)), +admin.autodiscover() + +urlpatterns = patterns('', + url(r'^$', home, name='home'), + url(r'^done/$', done, name='done'), + url(r'^logout/$', logout, name='logout'), + url(r'', include('social_auth.urls', namespace='social')), + url(r'^admin/', include(admin.site.urls)), ) diff --git a/social_auth/auth.py b/social_auth/auth.py new file mode 100644 index 0000000..d5c73eb --- /dev/null +++ b/social_auth/auth.py @@ -0,0 +1,218 @@ +import cgi +import urllib +import httplib +from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE +from openid.consumer.discover import DiscoveryFailure +from openid.extensions import sreg, ax + +from django.conf import settings +from django.utils import simplejson +from django.contrib.auth import authenticate + +from .base import BaseAuth +from .store import DjangoOpenIDStore +from .oauth import OAuthConsumer, OAuthToken, OAuthRequest, \ + OAuthSignatureMethod_HMAC_SHA1 +from .conf import AX_ATTRS, SREG_ATTR, OPENID_ID_FIELD, SESSION_NAME, \ + OPENID_GOOGLE_URL, OPENID_YAHOO_URL, TWITTER_SERVER, \ + TWITTER_REQUEST_TOKEN_URL, TWITTER_ACCESS_TOKEN_URL, \ + TWITTER_AUTHORIZATION_URL, TWITTER_CHECK_AUTH, \ + TWITTER_UNAUTHORIZED_TOKEN_NAME, FACEBOOK_CHECK_AUTH, \ + FACEBOOK_AUTHORIZATION_URL, FACEBOOK_ACCESS_TOKEN_URL + + +class OpenIdAuth(BaseAuth): + 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): + 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: + return authenticate(response=response, openid=True) + 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): + openid_request = self.openid_request() + # Request some user details. If the provider advertises support + # for attribute exchange, use that. + if openid_request.endpoint.supportsType(ax.AXMessage.ns_uri): + fetch_request = ax.FetchRequest() + # Mark all attributes as required, since 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): + if not hasattr(self, '_uses_redirect'): + setattr(self, '_uses_redirect', self.openid_request().shouldSendRedirect()) + return getattr(self, '_uses_redirect', True) + + def openid_request(self): + if not hasattr(self, '_openid_request'): + openid_url = self.openid_url() + try: + openid_request = self.consumer().begin(openid_url) + except DiscoveryFailure, e: + raise ValueError, 'OpenID discovery error: %s' % e + else: + setattr(self, '_openid_request', openid_request) + return getattr(self, '_openid_request', None) + + def openid_url(self): + 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""" + def openid_url(self): + return OPENID_GOOGLE_URL + + +class YahooAuth(OpenIdAuth): + """Yahoo OpenID authentication""" + def openid_url(self): + return OPENID_YAHOO_URL + + +class TwitterAuth(BaseAuth): + """Twitter OAuth authentication mechanism""" + def auth_url(self): + """Returns redirect url""" + token = self.unauthorized_token() + self.request.session[TWITTER_UNAUTHORIZED_TOKEN_NAME] = token.to_string() + return self.oauth_request(token, TWITTER_AUTHORIZATION_URL).to_url() + + def auth_complete(self): + """Returns user, might be logged in""" + unauthed_token = self.request.session.get(TWITTER_UNAUTHORIZED_TOKEN_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() + return authenticate(response=data, twitter=True) + + def unauthorized_token(self): + request = self.oauth_request(token=None, url=TWITTER_REQUEST_TOKEN_URL) + response = self.fetch_response(request) + return OAuthToken.from_string(response) + + def access_token(self, token): + request = self.oauth_request(token, TWITTER_ACCESS_TOKEN_URL) + return OAuthToken.from_string(self.fetch_response(request)) + + def user_data(self, access_token): + 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 oauth_request(self, token, url): + request = OAuthRequest.from_consumer_and_token(self.consumer, token=token, http_url=url) + request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer, token) + return request + + def fetch_response(self, request): + self.connection.request(request.http_method, request.to_url()) + response = self.connection.getresponse() + return response.read() + + @property + def connection(self): + conn = getattr(self, '_connection', None) + if conn is None: + conn = httplib.HTTPSConnection(TWITTER_SERVER) + setattr(self, '_connection', conn) + return conn + + @property + def consumer(self): + cons = getattr(self, '_consumer', None) + if cons is None: + cons = OAuthConsumer(settings.TWITTER_CONSUMER_KEY, + settings.TWITTER_CONSUMER_SECRET) + setattr(self, '_consumer', cons) + return cons + + +class FacebookAuth(BaseAuth): + def __init__(self, request, redirect): + super(FacebookAuth, self).__init__(request, redirect) + self.redirect_uri = self.request.build_absolute_uri(self.redirect) + if settings.DEBUG: + self.redirect_uri = self.redirect_uri.replace(':8000', '') + + def auth_url(self): + """Returns redirect url""" + args = {'client_id': settings.FACEBOOK_APP_ID, + 'redirect_uri': self.redirect_uri} + return FACEBOOK_AUTHORIZATION_URL + '?' + urllib.urlencode(args) + + def auth_complete(self): + """Returns user, might be logged in""" + if 'code' in self.request.GET: + args = {'client_id': settings.FACEBOOK_APP_ID, + 'redirect_uri': self.redirect_uri, + 'client_secret': settings.FACEBOOK_API_SECRET, + 'code': self.request.GET['code']} + url = FACEBOOK_ACCESS_TOKEN_URL + '?' + urllib.urlencode(args) + 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 + return authenticate(response=data, facebook=True) + else: + raise ValueError, 'Authentication error' + + def user_data(self, access_token): + params = {'access_token': access_token} + url = FACEBOOK_CHECK_AUTH + '?' + urllib.urlencode(params) + try: + return simplejson.load(urllib.urlopen(url)) + except simplejson.JSONDecodeError: + return None diff --git a/social_auth/backends.py b/social_auth/backends.py index 9511a05..21634d4 100644 --- a/social_auth/backends.py +++ b/social_auth/backends.py @@ -1,25 +1,23 @@ from openid.extensions import ax, sreg from .base import SocialAuthBackend -from .openid_auth import OLD_AX_ATTRS, AX_SCHEMA_ATTRS +from .conf import OLD_AX_ATTRS, AX_SCHEMA_ATTRS class OAuthBackend(SocialAuthBackend): """OAuth authentication backend base class""" - name = 'oauth' - def get_user_id(self, details, response): "OAuth providers return an unique user id in response""" return response['id'] -class TwitterOAuthBackend(OAuthBackend): +class TwitterBackend(OAuthBackend): """Twitter OAuth authentication backend""" name = 'twitter' def authenticate(self, **kwargs): if kwargs.pop('twitter', False): - return super(TwitterOAuthBackend, self).authenticate(**kwargs) + return super(TwitterBackend, self).authenticate(**kwargs) def get_user_details(self, response): return {'email': '', # not supplied @@ -29,13 +27,13 @@ class TwitterOAuthBackend(OAuthBackend): 'lastname': ''} -class FacebookOAuthBackend(OAuthBackend): +class FacebookBackend(OAuthBackend): """Facebook OAuth authentication backend""" name = 'facebook' def authenticate(self, **kwargs): if kwargs.pop('facebook', False): - return super(FacebookOAuthBackend, self).authenticate(**kwargs) + return super(FacebookBackend, self).authenticate(**kwargs) def get_user_details(self, response): return {'email': response.get('email', ''), @@ -58,17 +56,16 @@ class OpenIDBackend(SocialAuthBackend): return response.identity_url def get_user_details(self, response): - values = {'email': None, - 'username': None, - 'fullname': None, - 'firstname': None, - 'lastname': None} + values = {'email': '', + 'username': '', + 'fullname': '', + 'firstname': '', + 'lastname': ''} resp = sreg.SRegResponse.fromSuccessResponse(response) if resp: - values.update({'email': resp.get('email'), - 'fullname': resp.get('fullname'), - 'username': resp.get('nickname')}) + 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) @@ -76,9 +73,9 @@ class OpenIDBackend(SocialAuthBackend): values.update((alias.replace('old_', ''), resp.getSingle(src)) for src, alias in OLD_AX_ATTRS + AX_SCHEMA_ATTRS) - fullname = values.get('fullname', '') - firstname = values.get('firstname', '') - lastname = values.get('lastname', '') + fullname = values.get('fullname') or '' + firstname = values.get('firstname') or '' + lastname = values.get('lastname') or '' if not fullname and firstname and lastname: fullname = firstname + ' ' + lastname diff --git a/social_auth/conf.py b/social_auth/conf.py index ef13cb7..c1515de 100644 --- a/social_auth/conf.py +++ b/social_auth/conf.py @@ -1,16 +1,16 @@ # Twitter configuration -TWITTER_SERVER = 'api.twitter.com' -REQUEST_TOKEN_URL = 'https://%s/oauth/request_token' % TWITTER_SERVER -ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % TWITTER_SERVER -AUTHORIZATION_URL = 'http://%s/oauth/authorize' % TWITTER_SERVER -TWITTER_CHECK_AUTH = 'https://twitter.com/account/verify_credentials.json' -UNAUTHORIZED_TOKEN_NAME = 'twitter_unauthorized_token' +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 +TWITTER_AUTHORIZATION_URL = 'http://%s/oauth/authorize' % TWITTER_SERVER +TWITTER_CHECK_AUTH = 'https://twitter.com/account/verify_credentials.json' +TWITTER_UNAUTHORIZED_TOKEN_NAME = 'twitter_unauthorized_token' # Facebook configuration -FACEBOOK_SERVER = 'graph.facebook.com' -AUTHORIZATION_URL = 'https://%s/oauth/authorize' % FACEBOOK_SERVER -ACCESS_TOKEN_URL = 'https://%s/oauth/access_token' % FACEBOOK_SERVER -FACEBOOK_CHECK_AUTH = 'https://%s/me' % FACEBOOK_SERVER +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 # OpenID configuration OLD_AX_ATTRS = [ diff --git a/social_auth/models.py b/social_auth/models.py index 21baecb..106e60b 100644 --- a/social_auth/models.py +++ b/social_auth/models.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User class UserSocialAuth(models.Model): + """Social Auth association model""" user = models.ForeignKey(User) provider = models.CharField(max_length=32) uid = models.CharField(max_length=2048) diff --git a/social_auth/urls.py b/social_auth/urls.py index 1572f42..a7e1312 100644 --- a/social_auth/urls.py +++ b/social_auth/urls.py @@ -1,14 +1,9 @@ from django.conf.urls.defaults import patterns, url -from .views import home, done, logout, auth, complete +from .views import auth, complete urlpatterns = patterns('', url(r'^login/(?P[^/]+)/$', auth, name='begin'), url(r'^complete/(?P[^/]+)/$', complete, name='complete'), - - # demo urls - url(r'^$', home, name='home'), - url(r'^done/$', done, name='done'), - url(r'^logout/$', logout, name='logout'), ) diff --git a/social_auth/views.py b/social_auth/views.py index 1a3cc7b..6cf0e00 100644 --- a/social_auth/views.py +++ b/social_auth/views.py @@ -2,20 +2,17 @@ from django.conf import settings from django.http import HttpResponseRedirect, HttpResponse, \ HttpResponseServerError from django.core.urlresolvers import reverse -from django.contrib.auth import login, logout as auth_logout, REDIRECT_FIELD_NAME -from django.contrib.auth.decorators import login_required +from django.contrib.auth import login, REDIRECT_FIELD_NAME -from .twitter import TwitterOAuth -from .facebook import FacebookOAuth -from .openid_auth import OpenIDAuth, GoogleAuth, YahooAuth +from .auth import TwitterAuth, FacebookAuth, OpenIdAuth, GoogleAuth, YahooAuth BACKENDS = { - 'twitter': TwitterOAuth, - 'facebook': FacebookOAuth, + 'twitter': TwitterAuth, + 'facebook': FacebookAuth, 'google': GoogleAuth, 'yahoo': YahooAuth, - 'openid': OpenIDAuth, + 'openid': OpenIdAuth, } @@ -42,35 +39,3 @@ def complete(request, backend): login(request, user) return HttpResponseRedirect(request.session.pop(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL)) - - -def home(request): - return HttpResponse( - """ -
-

OAuth

- twitter - facebook -
- -
-

OPenID

- google - yahoo -
- provider: - -
-
-
- logout - """, content_type='text/html;charset=UTF-8') - -@login_required -def done(request): - user = request.user - return HttpResponse('%s / %s / %s' % (user.id, user.username, user.first_name)) - -def logout(request): - auth_logout(request) - return HttpResponse('logged out') -- 2.39.5