'social_auth.backends.twitter.TwitterBackend',
'social_auth.backends.facebook.FacebookBackend',
'social_auth.backends.google.GoogleOAuthBackend',
+ 'social_auth.backends.google.GoogleOAuth2Backend',
'social_auth.backends.google.GoogleBackend',
'social_auth.backends.yahoo.YahooBackend',
'social_auth.backends.contrib.linkedin.LinkedinBackend',
GOOGLE_OAUTH_EXTRA_SCOPE = [...]
-check which Apps are included in their `Google Data Protocol Directory`_
+Check which applications can be included in their `Google Data Protocol Directory`_
+
+-------------
+Google OAuth2
+-------------
+Recently Google launched OAuth2 support following the definition at
+`OAuth2 draft`. It works in a similar way to plain OAuth mechanism, but
+developers *must* register an application and apply for a set of keys. Check
+`Google OAuth2`_ document for details.
+
+To enable OAuth2 support:
+
+- fill "Client Key" and "Client Secret" settings, these values can be obtained
+ easily as described on `OAuth2 Registering`_ doc::
+
+ GOOGLE_OAUTH2_CLIENT_KEY = ''
+ GOOGLE_OAUTH2_CLIENT_SECRET = ''
+
+- scopes are shared between OAuth mechanisms::
+
+ GOOGLE_OAUTH_EXTRA_SCOPE = [...]
+
+Check which applications can be included in their `Google Data Protocol Directory`_
+
-------
Testing
.. _Orkut API: http://code.google.com/apis/orkut/docs/rest/developers_guide_protocol.html#Authenticating
.. _Google OpenID: http://code.google.com/apis/accounts/docs/OpenID.html
.. _Google OAuth: http://code.google.com/apis/accounts/docs/OAuth.html
+.. _Google OAuth2: http://code.google.com/apis/accounts/docs/OAuth2.html
+.. _OAuth2 Registering: http://code.google.com/apis/accounts/docs/OAuth2.html#Registering
.. _Google Data Protocol Directory: http://code.google.com/apis/gdata/docs/directory.html
+.. _OAuth2 draft: http://tools.ietf.org/html/draft-ietf-oauth-v2-10
.. _OAuth reference: http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth
.. _Yahoo OpenID: http://openid.yahoo.com/
.. _Twitter OAuth: http://dev.twitter.com/pages/oauth_faq
LINKEDIN_CONSUMER_SECRET = ''
ORKUT_CONSUMER_KEY = ''
ORKUT_CONSUMER_SECRET = ''
+GOOGLE_OAUTH2_CLIENT_KEY = ''
+GOOGLE_OAUTH2_CLIENT_SECRET = ''
SOCIAL_AUTH_CREATE_USERS = True
SOCIAL_AUTH_FORCE_RANDOM_USERNAME = False
SOCIAL_AUTH_DEFAULT_USERNAME = 'socialauth_user'
'social_auth.backends.twitter.TwitterBackend',
'social_auth.backends.facebook.FacebookBackend',
'social_auth.backends.google.GoogleOAuthBackend',
+ 'social_auth.backends.google.GoogleOAuth2Backend',
'social_auth.backends.google.GoogleBackend',
'social_auth.backends.yahoo.YahooBackend',
'social_auth.backends.contrib.linkedin.LinkedinBackend',
{% if orkut %}<span class="disconnect">(<a href="{% url disconnect "orkut" %}">disconnect</a>)</span>{% endif %}
</li>
<li>
- <a rel="nofollow" href="{% url associate_begin "google-oauth" %}">Google</a>
+ <a rel="nofollow" href="{% url associate_begin "google-oauth" %}">Google OAuth</a>
{% if google_oauth %}<span class="disconnect">(<a href="{% url disconnect "google-oauth" %}">disconnect</a>)</span>{% endif %}
</li>
</ul>
+ <h3>Associate new OAuth2 credentials:</h3>
+ <ul>
+ <li>
+ <a rel="nofollow" href="{% url associate_begin "google-oauth2" %}">Google OAuth2</a>
+ {% if google_oauth2 %}<span class="disconnect">(<a href="{% url disconnect "google-oauth2" %}">disconnect</a>)</span>{% endif %}
+ </li>
+ </ul>
+
<h3>Associate new OpenID credentials:</h3>
<ul>
<li>
- <a rel="nofollow" href="{% url associate_begin "google" %}">Google</a>
+ <a rel="nofollow" href="{% url associate_begin "google" %}">Google OpenID</a>
{% if google %}<span class="disconnect">(<a href="{% url disconnect "google" %}">disconnect</a>)</span>{% endif %}
</li>
<li>
</ul>
</div>
+<div>
+ <h3>Login using <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-10" title="OAuth2">OAuth2</a> from:</h3>
+ <ul>
+ <li><a rel="nofollow" href="{% url begin "google-oauth2" %}">Google OAuth2</a></li>
+ </ul>
+</div>
+
<div>
<h3>Login using <a href="http://openid.net/" title="OpenId">OpenId</a> from:</h3>
<ul>
"""
from os import urandom, walk
from os.path import basename
+from urllib2 import Request, urlopen
+from urllib import urlencode
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 oauth2 import Consumer as OAuthConsumer, \
- Token as OAuthToken, \
- Request as OAuthRequest, \
- SignatureMethod_HMAC_SHA1 as OAuthSignatureMethod_HMAC_SHA1
+from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest, \
+ SignatureMethod_HMAC_SHA1
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.backends import ModelBackend
+from django.utils import simplejson
from django.utils.hashcompat import md5_constructor
from django.utils.importlib import import_module
SETTINGS_SECRET_NAME = ''
def auth_url(self):
- """Returns redirect url"""
+ """Return 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"""
+ """Return 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)
+ token = Token.from_string(unauthed_token)
if token.key != self.data.get('oauth_token', 'no-token'):
raise ValueError('Incorrect tokens')
"""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)
+ return Token.from_string(response)
def oauth_request(self, token, url, extra_params=None):
"""Generate OAuth request, setups callback url"""
token=token,
http_url=url,
parameters=params)
- request.sign_request(OAuthSignatureMethod_HMAC_SHA1(), self.consumer,
- token)
+ request.sign_request(SignatureMethod_HMAC_SHA1(), self.consumer, token)
return request
def fetch_response(self, request):
"""Executes request and fetchs service response"""
- self.connection.request(request.method, request.to_url())
- response = self.connection.getresponse()
- return response.read()
+ connection = HTTPSConnection(self.SERVER_URL)
+ connection.request(request.method.upper(), request.to_url())
+ return connection.getresponse().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))
+ return Token.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
+ consumer = getattr(self, '_consumer', None)
+ if consumer is None:
+ consumer = OAuthConsumer(*self.get_key_and_secret())
+ setattr(self, '_consumer', consumer)
+ return consumer
def get_key_and_secret(self):
"""Return tuple with Consumer Key and Consumer Secret for current
def enabled(cls):
"""Return backend enabled status by checking basic settings"""
return all(hasattr(settings, name) for name in
- (cls.SETTINGS_KEY_NAME,
- cls.SETTINGS_SECRET_NAME))
+ (cls.SETTINGS_KEY_NAME, cls.SETTINGS_SECRET_NAME))
+
+
+class BaseOAuth2(BaseOAuth):
+ """Base class for OAuth2 providers.
+
+ OAuth2 draft details at:
+ http://tools.ietf.org/html/draft-ietf-oauth-v2-10
+
+ Attributes:
+ @AUTHORIZATION_URL Authorization service url
+ @ACCESS_TOKEN_URL Token URL
+ """
+ AUTHORIZATION_URL = None
+ ACCESS_TOKEN_URL = None
+
+ def auth_url(self):
+ """Return redirect url"""
+ client_id, client_secret = self.get_key_and_secret()
+ args = {'client_id': client_id,
+ 'scope': ' '.join(self.get_scope()),
+ 'redirect_uri': self.redirect_uri,
+ 'response_type': 'code'} # requesting code
+ return self.AUTHORIZATION_URL + '?' + urlencode(args)
+
+ def auth_complete(self, *args, **kwargs):
+ """Completes loging process, must return user instance"""
+ client_id, client_secret = self.get_key_and_secret()
+ params = {'grant_type': 'authorization_code', # request auth code
+ 'code': self.data.get('code', ''), # server response code
+ 'client_id': client_id,
+ 'client_secret': client_secret,
+ 'redirect_uri': self.redirect_uri}
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ request = Request(self.ACCESS_TOKEN_URL, data=urlencode(params),
+ headers=headers)
+
+ try:
+ response = simplejson.loads(urlopen(request).read())
+ except (simplejson.JSONDecodeError, KeyError):
+ raise ValueError('Unknown OAuth2 response type')
+
+ if response.get('error'):
+ error = response.get('error_description') or response.get('error')
+ raise ValueError('OAuth2 authentication failed: %s' % error)
+ else:
+ response.update(self.user_data(response['access_token']) or {})
+ kwargs.update({'response': response, self.AUTH_BACKEND.name: True})
+ return authenticate(*args, **kwargs)
+
+ def get_scope(self):
+ """Return list with needed access scope"""
+ return []
+
+ def get_key_and_secret(self):
+ """Return tuple with Consumer Key and Consumer Secret for current
+ service provider. Must return (key, secret), order *must* be respected.
+ """
+ return getattr(settings, self.SETTINGS_KEY_NAME), \
+ getattr(settings, self.SETTINGS_SECRET_NAME)
# import sources from where check for auth backends
Setting GOOGLE_OAUTH_EXTRA_SCOPE can be used to access different user
related data, like calendar, contacts, docs, etc.
+OAuth2 works similar to OAuth but application must be defined on Google
+APIs console https://code.google.com/apis/console/ Identity option.
+
OpenID also works straightforward, it doesn't need further configurations.
"""
+from urllib import urlencode
from urllib2 import Request, urlopen
from django.conf import settings
from django.utils import simplejson
-from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, \
+from social_auth.backends import OpenIdAuth, ConsumerBasedOAuth, BaseOAuth2, \
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'
+GOOGLE_OAUTH_SERVER = 'www.google.com'
+GOOGLE_OAUTH_AUTHORIZATION_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
+GOOGLE_OAUTH_REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
+GOOGLE_OAUTH_ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
+
+# Google OAuth2 base configuration
+GOOGLE_OAUTH2_SERVER = 'accounts.google.com'
+GOOGLE_OATUH2_AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth'
+
# 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'
+EXPIRES_NAME = getattr(settings, 'SOCIAL_AUTH_EXPIRATION', 'expires')
+
# Backends
class GoogleOAuthBackend(OAuthBackend):
'last_name': ''}
+class GoogleOAuth2Backend(GoogleOAuthBackend):
+ """Google OAuth2 authentication backend"""
+ name = 'google-oauth2'
+ EXTRA_DATA = [('refresh_token', 'refresh_token'),
+ ('expires_in', EXPIRES_NAME)]
+
+
class GoogleBackend(OpenIDBackend):
"""Google OpenID authentication backend"""
name = 'google'
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
+ AUTHORIZATION_URL = GOOGLE_OAUTH_AUTHORIZATION_URL
+ REQUEST_TOKEN_URL = GOOGLE_OAUTH_REQUEST_TOKEN_URL
+ ACCESS_TOKEN_URL = GOOGLE_OAUTH_ACCESS_TOKEN_URL
+ SERVER_URL = GOOGLE_OAUTH_SERVER
def user_data(self, access_token):
"""Loads user data from G service"""
SETTINGS_SECRET_NAME = 'GOOGLE_CONSUMER_SECRET'
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
+ """Return user data from Google API"""
+ url = self.oauth_request(access_token, GOOGLEAPIS_EMAIL).to_url()
+ return googleapis_email(url.split('?')[1])
def oauth_request(self, token, url, extra_params=None):
extra_params = extra_params or {}
return True
+class GoogleOAuth2(BaseOAuth2):
+ """Google OAuth2 support"""
+ AUTH_BACKEND = GoogleOAuth2Backend
+ AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/auth'
+ ACCESS_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
+ SETTINGS_KEY_NAME = 'GOOGLE_OAUTH2_CLIENT_KEY'
+ SETTINGS_SECRET_NAME = 'GOOGLE_OAUTH2_CLIENT_SECRET'
+
+ def get_scope(self):
+ return GOOGLE_OAUTH_SCOPE + \
+ getattr(settings, 'GOOGLE_OAUTH_EXTRA_SCOPE', [])
+
+ def user_data(self, access_token):
+ """Return user data from Google API"""
+ return googleapis_email(urlencode({'oauth_token': access_token}))
+
+
+def googleapis_email(params):
+ """Loads user data from googleapis service, only email so far as it's
+ described in http://sites.google.com/site/oauthgoog/Home/emaildisplayscope
+
+ Parameters must be passed in queryset and Authorization header as described
+ on Google OAuth documentation at:
+ http://groups.google.com/group/oauth/browse_thread/thread/d15add9beb418ebc
+ and:
+ http://code.google.com/apis/accounts/docs/OAuth2.html#CallingAnAPI
+ """
+ request = Request(GOOGLEAPIS_EMAIL + '?alt=json&' + params,
+ headers={'Authorization': params})
+ try:
+ return simplejson.loads(urlopen(request).read())['data']
+ except (simplejson.JSONDecodeError, KeyError, IOError):
+ return None
+
+
# Backend definition
BACKENDS = {
'google': GoogleAuth,
'google-oauth': GoogleOAuth,
+ 'google-oauth2': GoogleOAuth2,
}