1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
Adding a new backend
====================
Add new backends is quite easy, usually adding just a ``class`` with a couple
settings and methods overrides to retrieve user data from services API. Follow
the details below.
Common attributes
-----------------
First, lets check the common attributes for all backend types.
``name = ''``
Any backend needs a name, usually the popular name of the service is used,
like ``facebook``, ``twitter``, etc. It must be unique, otherwise another
backend can take precedence if it's listed before in
``AUTHENTICATION_BACKENDS`` setting.
``ID_KEY = None``
Defines the attribute in the service response that identifies the user as
unique in the service, the value is later stored in the ``uid`` attribute
in the ``UserSocialAuth`` instance.
``REQUIRES_EMAIL_VALIDATION = False``
Flags the backend to enforce email validation during the pipeline (if the
corresponding pipeline ``social.pipeline.mail.mail_validation`` was
enabled).
``EXTRA_DATA = None``
During the auth process some basic user data is returned by the provider or
retrieved by ``user_data()`` method which usually is used to call some API
on the provider to retrieve it. This data will be stored under
``UserSocialAuth.extra_data`` attribute, but to make it accessible under
some common names on different providers, this attribute defines a list of
tuples in the form ``(name, alias)`` where ``name`` is the key in the user
data (which should be a ``dict`` instance) and ``alias`` is the name to
store it on ``extra_data``.
OAuth
-----
OAuth1 and OAuth2 provide share some common definitions based on the shared
behavior during the auth process, like a successful API response from
``AUTHORIZATION_URL`` usually returns some basic user data like a user Id.
Shared attributes
*****************
``name``
This defines the backend name and identifies it during the auth process.
The name is used in the URLs ``/login/<backend name>`` and
``/complete/<backend name>``.
``ID_KEY = 'id'``
Default key name where user identification field is defined, it's used on
auth process when some basic user data is returned. This Id is stored in
``UserSocialAuth.uid`` field, this together the ``UserSocialAuth.provider``
field is used to unique identify a user association.
``SCOPE_PARAMETER_NAME = 'scope'``
Scope argument is used to tell the provider the API endpoints you want to
call later, it's a permissions request granted over the ``access_token``
later retrieved. Default value is ``scope`` since that's usually the name
used in the URL parameter, but can be overridden if needed.
``DEFAULT_SCOPE = None``
Some providers give nothing about the user but some basic data in required
like the user Id or an email address. Default scope attribute is used to
specify a default value for ``scope`` argument to request those extra used
bits.
``SCOPE_SEPARATOR = ' '``
The ``scope`` argument is usually a list of permissions to request, the
list is joined used a separator, usually just a blank space, but differ
from provider to provider, override the default value with this attribute
if it differs.
OAuth2
******
OAuth2 backends are fairly simple to implement; just a few settings, a method
override and it's mostly ready to go.
The key points on this backends are:
``AUTHORIZATION_URL``
This is the entry point for the authorization mechanism, users must be
redirected to this URL, used on ``auth_url`` method which builds the
redirect address with ``AUTHORIZATION_URL`` plus some arguments
(``client_id``, ``redirect_uri``, ``response_type``, and ``state``).
``ACCESS_TOKEN_URL``
Must point to the API endpoint that provides an ``access_token`` needed to
authenticate in users behalf on future API calls.
``REFRESH_TOKEN_URL``
Some providers give the option to renew the ``access_token`` since they are
usually limited in time, once that time runs out, the token is invalidated
and cannot be used any more. This attribute should point to that API
endpoint.
``RESPONSE_TYPE``
The response type expected on the auth process, default value is ``code``
as dictated by OAuth2 definition. Override it if default value doesn't fit
the provider implementation.
``STATE_PARAMETER``
OAuth2 defines that an ``state`` parameter can be passed in order to
validate the process, it's kind of a CSRF check to avoid man in the middle
attacks. Some don't recognise it or don't return it which will make the
auth process invalid. Set this attribute to ``False`` in that case.
``REDIRECT_STATE``
For those providers that don't recognise the ``state`` parameter, the app
can add a ``redirect_state`` argument to the ``redirect_uri`` to mimic it.
Set this value to ``False`` if the provider likes to verify the
``redirect_uri`` value and this parameter invalidates that check.
Example code::
from social.backends.oauth import BaseOAuth2
class GithubOAuth2(BaseOAuth2):
"""Github OAuth authentication backend"""
name = 'github'
AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize'
ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'
SCOPE_SEPARATOR = ','
EXTRA_DATA = [
('id', 'id'),
('expires', 'expires')
]
def get_user_details(self, response):
"""Return user details from Github account"""
return {'username': response.get('login'),
'email': response.get('email') or '',
'first_name': response.get('name')}
def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service"""
url = 'https://api.github.com/user?' + urlencode({
'access_token': access_token
})
try:
return json.load(self.urlopen(url))
except ValueError:
return None
OAuth1
******
OAuth1 process is a bit more trickier, `Twitter Docs`_ explains it quite well.
Beside the ``AUTHORIZATION_URL`` and ``ACCESS_TOKEN_URL`` attributes, a third
one is needed used when starting the process.
``REQUEST_TOKEN_URL = ''``
During the auth process an unauthorized token is needed to start the
process, later this token is exchanged for an ``access_token``. This
setting points to the API endpoint where that unauthorized token can be
retrieved.
Example code::
from xml.dom import minidom
from social.backends.oauth import ConsumerBasedOAuth
class TripItOAuth(ConsumerBasedOAuth):
"""TripIt OAuth authentication backend"""
name = 'tripit'
AUTHORIZATION_URL = 'https://www.tripit.com/oauth/authorize'
REQUEST_TOKEN_URL = 'https://api.tripit.com/oauth/request_token'
ACCESS_TOKEN_URL = 'https://api.tripit.com/oauth/access_token'
EXTRA_DATA = [('screen_name', 'screen_name')]
def get_user_details(self, response):
"""Return user details from TripIt account"""
try:
first_name, last_name = response['name'].split(' ', 1)
except ValueError:
first_name = response['name']
last_name = ''
return {'username': response['screen_name'],
'email': response['email'],
'fullname': response['name'],
'first_name': first_name,
'last_name': last_name}
def user_data(self, access_token, *args, **kwargs):
"""Return user data provided"""
url = 'https://api.tripit.com/v1/get/profile'
request = self.oauth_request(access_token, url)
content = self.fetch_response(request)
try:
dom = minidom.parseString(content)
except ValueError:
return None
return {
'id': dom.getElementsByTagName('Profile')[0].getAttribute('ref'),
'name': dom.getElementsByTagName(
'public_display_name')[0].childNodes[0].data,
'screen_name': dom.getElementsByTagName(
'screen_name')[0].childNodes[0].data,
'email': dom.getElementsByTagName(
'is_primary')[0].parentNode.getElementsByTagName(
'address')[0].childNodes[0].data,
}
OpenId
------
OpenId is fair simpler that OAuth since it's used for authentication rather
than authorization (regardless it's used for authorization too).
A single attribute is usually needed, the authentication URL endpoint.
``URL = ''``
OpenId endpoint where to redirect the user.
Sometimes the URL is user dependant, like in myOpenId_ where the URL is
``https://<user handler>.myopenid.com``. For those cases where the user must
input it's handle (or full URL). The backend must override the ``openid_url()``
method to retrieve it and return a full URL to where the user will be
redirected.
Example code::
from social.backends.open_id import OpenIdAuth
from social.exceptions import AuthMissingParameter
class LiveJournalOpenId(OpenIdAuth):
"""LiveJournal OpenID authentication backend"""
name = 'livejournal'
def get_user_details(self, response):
"""Generate username from identity url"""
values = super(LiveJournalOpenId, self).get_user_details(response)
values['username'] = values.get('username') or \
urlparse.urlsplit(response.identity_url)\
.netloc.split('.', 1)[0]
return values
def openid_url(self):
"""Returns LiveJournal authentication URL"""
if not self.data.get('openid_lj_user'):
raise AuthMissingParameter(self, 'openid_lj_user')
return 'http://%s.livejournal.com' % self.data['openid_lj_user']
Auth APIs
---------
For others authentication types, a ``BaseAuth`` class is defined to help. Those
custom auth methods must override the ``auth_url()`` and ``auth_complete()``
methods.
Example code::
from google.appengine.api import users
from social.backends.base import BaseAuth
from social.exceptions import AuthException
class GoogleAppEngineAuth(BaseAuth):
"""GoogleAppengine authentication backend"""
name = 'google-appengine'
def get_user_id(self, details, response):
"""Return current user id."""
user = users.get_current_user()
if user:
return user.user_id()
def get_user_details(self, response):
"""Return user basic information (id and email only)."""
user = users.get_current_user()
return {'username': user.user_id(),
'email': user.email(),
'fullname': '',
'first_name': '',
'last_name': ''}
def auth_url(self):
"""Build and return complete URL."""
return users.create_login_url(self.redirect_uri)
def auth_complete(self, *args, **kwargs):
"""Completes login process, must return user instance."""
if not users.get_current_user():
raise AuthException('Authentication error')
kwargs.update({'response': '', 'backend': self})
return self.strategy.authenticate(*args, **kwargs)
.. _Twitter Docs: https://dev.twitter.com/docs/auth/implementing-sign-twitter
.. _myOpenId: https://www.myopenid.com/
|