File: implementation.rst

package info (click to toggle)
python-social-auth 0.2.1-2
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 2,828 kB
  • ctags: 3,245
  • sloc: python: 12,867; makefile: 119; sh: 3
file content (308 lines) | stat: -rw-r--r-- 11,721 bytes parent folder | download | duplicates (2)
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/