~0x44/nova/extdoc

« back to all changes in this revision

Viewing changes to vendor/tornado/tornado/auth.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
#
 
3
# Copyright 2009 Facebook
 
4
#
 
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
 
6
# not use this file except in compliance with the License. You may obtain
 
7
# a copy of the License at
 
8
#
 
9
#     http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
# Unless required by applicable law or agreed to in writing, software
 
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
14
# License for the specific language governing permissions and limitations
 
15
# under the License.
 
16
 
 
17
"""Implementations of various third-party authentication schemes.
 
18
 
 
19
All the classes in this file are class Mixins designed to be used with
 
20
web.py RequestHandler classes. The primary methods for each service are
 
21
authenticate_redirect(), authorize_redirect(), and get_authenticated_user().
 
22
The former should be called to redirect the user to, e.g., the OpenID
 
23
authentication page on the third party service, and the latter should
 
24
be called upon return to get the user data from the data returned by
 
25
the third party service.
 
26
 
 
27
They all take slightly different arguments due to the fact all these
 
28
services implement authentication and authorization slightly differently.
 
29
See the individual service classes below for complete documentation.
 
30
 
 
31
Example usage for Google OpenID:
 
32
 
 
33
class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
 
34
    @tornado.web.asynchronous
 
35
    def get(self):
 
36
        if self.get_argument("openid.mode", None):
 
37
            self.get_authenticated_user(self.async_callback(self._on_auth))
 
38
            return
 
39
        self.authenticate_redirect()
 
40
    
 
41
    def _on_auth(self, user):
 
42
        if not user:
 
43
            raise tornado.web.HTTPError(500, "Google auth failed")
 
44
        # Save the user with, e.g., set_secure_cookie()
 
45
 
 
46
"""
 
47
 
 
48
import base64
 
49
import binascii
 
50
import cgi
 
51
import hashlib
 
52
import hmac
 
53
import httpclient
 
54
import escape
 
55
import logging
 
56
import time
 
57
import urllib
 
58
import urlparse
 
59
import uuid
 
60
 
 
61
_log = logging.getLogger("tornado.auth")
 
62
 
 
63
class OpenIdMixin(object):
 
64
    """Abstract implementation of OpenID and Attribute Exchange.
 
65
 
 
66
    See GoogleMixin below for example implementations.
 
67
    """
 
68
    def authenticate_redirect(self, callback_uri=None,
 
69
                              ax_attrs=["name","email","language","username"]):
 
70
        """Returns the authentication URL for this service.
 
71
 
 
72
        After authentication, the service will redirect back to the given
 
73
        callback URI.
 
74
 
 
75
        We request the given attributes for the authenticated user by
 
76
        default (name, email, language, and username). If you don't need
 
77
        all those attributes for your app, you can request fewer with
 
78
        the ax_attrs keyword argument.
 
79
        """
 
80
        callback_uri = callback_uri or self.request.path
 
81
        args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
 
82
        self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
 
83
 
 
84
    def get_authenticated_user(self, callback):
 
85
        """Fetches the authenticated user data upon redirect.
 
86
 
 
87
        This method should be called by the handler that receives the
 
88
        redirect from the authenticate_redirect() or authorize_redirect()
 
89
        methods.
 
90
        """
 
91
        # Verify the OpenID response via direct request to the OP
 
92
        args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems())
 
93
        args["openid.mode"] = u"check_authentication"
 
94
        url = self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)
 
95
        http = httpclient.AsyncHTTPClient()
 
96
        http.fetch(url, self.async_callback(
 
97
            self._on_authentication_verified, callback))
 
98
 
 
99
    def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
 
100
        url = urlparse.urljoin(self.request.full_url(), callback_uri)
 
101
        args = {
 
102
            "openid.ns": "http://specs.openid.net/auth/2.0",
 
103
            "openid.claimed_id": 
 
104
                "http://specs.openid.net/auth/2.0/identifier_select",
 
105
            "openid.identity": 
 
106
                "http://specs.openid.net/auth/2.0/identifier_select",
 
107
            "openid.return_to": url,
 
108
            "openid.realm": "http://" + self.request.host + "/",
 
109
            "openid.mode": "checkid_setup",
 
110
        }
 
111
        if ax_attrs:
 
112
            args.update({
 
113
                "openid.ns.ax": "http://openid.net/srv/ax/1.0",
 
114
                "openid.ax.mode": "fetch_request",
 
115
            })
 
116
            ax_attrs = set(ax_attrs)
 
117
            required = []
 
118
            if "name" in ax_attrs:
 
119
                ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
 
120
                required += ["firstname", "fullname", "lastname"]
 
121
                args.update({
 
122
                    "openid.ax.type.firstname":
 
123
                        "http://axschema.org/namePerson/first",
 
124
                    "openid.ax.type.fullname":
 
125
                        "http://axschema.org/namePerson",
 
126
                    "openid.ax.type.lastname":
 
127
                        "http://axschema.org/namePerson/last",
 
128
                })
 
129
            known_attrs = {
 
130
                "email": "http://axschema.org/contact/email",
 
131
                "language": "http://axschema.org/pref/language",
 
132
                "username": "http://axschema.org/namePerson/friendly",
 
133
            }
 
134
            for name in ax_attrs:
 
135
                args["openid.ax.type." + name] = known_attrs[name]
 
136
                required.append(name)
 
137
            args["openid.ax.required"] = ",".join(required)
 
138
        if oauth_scope:
 
139
            args.update({
 
140
                "openid.ns.oauth":
 
141
                    "http://specs.openid.net/extensions/oauth/1.0",
 
142
                "openid.oauth.consumer": self.request.host.split(":")[0],
 
143
                "openid.oauth.scope": oauth_scope,
 
144
            })
 
145
        return args
 
146
 
 
147
    def _on_authentication_verified(self, callback, response):
 
148
        if response.error or u"is_valid:true" not in response.body:
 
149
            _log.warning("Invalid OpenID response: %s", response.error or
 
150
                            response.body)
 
151
            callback(None)
 
152
            return
 
153
 
 
154
        # Make sure we got back at least an email from attribute exchange
 
155
        ax_ns = None
 
156
        for name, values in self.request.arguments.iteritems():
 
157
            if name.startswith("openid.ns.") and \
 
158
               values[-1] == u"http://openid.net/srv/ax/1.0":
 
159
                ax_ns = name[10:]
 
160
                break
 
161
        def get_ax_arg(uri):
 
162
            if not ax_ns: return u""
 
163
            prefix = "openid." + ax_ns + ".type."
 
164
            ax_name = None
 
165
            for name, values in self.request.arguments.iteritems():
 
166
                if values[-1] == uri and name.startswith(prefix):
 
167
                    part = name[len(prefix):]
 
168
                    ax_name = "openid." + ax_ns + ".value." + part
 
169
                    break
 
170
            if not ax_name: return u""
 
171
            return self.get_argument(ax_name, u"")
 
172
 
 
173
        email = get_ax_arg("http://axschema.org/contact/email")
 
174
        name = get_ax_arg("http://axschema.org/namePerson")
 
175
        first_name = get_ax_arg("http://axschema.org/namePerson/first")
 
176
        last_name = get_ax_arg("http://axschema.org/namePerson/last")
 
177
        username = get_ax_arg("http://axschema.org/namePerson/friendly")
 
178
        locale = get_ax_arg("http://axschema.org/pref/language").lower()
 
179
        user = dict()
 
180
        name_parts = []
 
181
        if first_name:
 
182
            user["first_name"] = first_name
 
183
            name_parts.append(first_name)
 
184
        if last_name:
 
185
            user["last_name"] = last_name
 
186
            name_parts.append(last_name)
 
187
        if name:
 
188
            user["name"] = name
 
189
        elif name_parts:
 
190
            user["name"] = u" ".join(name_parts)
 
191
        elif email:
 
192
            user["name"] = email.split("@")[0]
 
193
        if email: user["email"] = email
 
194
        if locale: user["locale"] = locale
 
195
        if username: user["username"] = username
 
196
        callback(user)
 
197
 
 
198
 
 
199
class OAuthMixin(object):
 
200
    """Abstract implementation of OAuth.
 
201
 
 
202
    See TwitterMixin and FriendFeedMixin below for example implementations.
 
203
    """
 
204
    def authorize_redirect(self, callback_uri=None):
 
205
        """Redirects the user to obtain OAuth authorization for this service.
 
206
 
 
207
        Twitter and FriendFeed both require that you register a Callback
 
208
        URL with your application. You should call this method to log the
 
209
        user in, and then call get_authenticated_user() in the handler
 
210
        you registered as your Callback URL to complete the authorization
 
211
        process.
 
212
 
 
213
        This method sets a cookie called _oauth_request_token which is
 
214
        subsequently used (and cleared) in get_authenticated_user for
 
215
        security purposes.
 
216
        """
 
217
        if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
 
218
            raise Exception("This service does not support oauth_callback")
 
219
        http = httpclient.AsyncHTTPClient()
 
220
        http.fetch(self._oauth_request_token_url(), self.async_callback(
 
221
            self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri))
 
222
 
 
223
    def get_authenticated_user(self, callback):
 
224
        """Gets the OAuth authorized user and access token on callback.
 
225
 
 
226
        This method should be called from the handler for your registered
 
227
        OAuth Callback URL to complete the registration process. We call
 
228
        callback with the authenticated user, which in addition to standard
 
229
        attributes like 'name' includes the 'access_key' attribute, which
 
230
        contains the OAuth access you can use to make authorized requests
 
231
        to this service on behalf of the user.
 
232
        """
 
233
        request_key = self.get_argument("oauth_token")
 
234
        request_cookie = self.get_cookie("_oauth_request_token")
 
235
        if not request_cookie:
 
236
            _log.warning("Missing OAuth request token cookie")
 
237
            callback(None)
 
238
            return
 
239
        cookie_key, cookie_secret = request_cookie.split("|")
 
240
        if cookie_key != request_key:
 
241
            _log.warning("Request token does not match cookie")
 
242
            callback(None)
 
243
            return
 
244
        token = dict(key=cookie_key, secret=cookie_secret)
 
245
        http = httpclient.AsyncHTTPClient()
 
246
        http.fetch(self._oauth_access_token_url(token), self.async_callback(
 
247
            self._on_access_token, callback))
 
248
 
 
249
    def _oauth_request_token_url(self):
 
250
        consumer_token = self._oauth_consumer_token()
 
251
        url = self._OAUTH_REQUEST_TOKEN_URL
 
252
        args = dict(
 
253
            oauth_consumer_key=consumer_token["key"],
 
254
            oauth_signature_method="HMAC-SHA1",
 
255
            oauth_timestamp=str(int(time.time())),
 
256
            oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
 
257
            oauth_version="1.0",
 
258
        )
 
259
        signature = _oauth_signature(consumer_token, "GET", url, args)
 
260
        args["oauth_signature"] = signature
 
261
        return url + "?" + urllib.urlencode(args)
 
262
 
 
263
    def _on_request_token(self, authorize_url, callback_uri, response):
 
264
        if response.error:
 
265
            raise Exception("Could not get request token")
 
266
        request_token = _oauth_parse_response(response.body)
 
267
        data = "|".join([request_token["key"], request_token["secret"]])
 
268
        self.set_cookie("_oauth_request_token", data)
 
269
        args = dict(oauth_token=request_token["key"])
 
270
        if callback_uri:
 
271
            args["oauth_callback"] = urlparse.urljoin(
 
272
                self.request.full_url(), callback_uri)
 
273
        self.redirect(authorize_url + "?" + urllib.urlencode(args))
 
274
 
 
275
    def _oauth_access_token_url(self, request_token):
 
276
        consumer_token = self._oauth_consumer_token()
 
277
        url = self._OAUTH_ACCESS_TOKEN_URL
 
278
        args = dict(
 
279
            oauth_consumer_key=consumer_token["key"],
 
280
            oauth_token=request_token["key"],
 
281
            oauth_signature_method="HMAC-SHA1",
 
282
            oauth_timestamp=str(int(time.time())),
 
283
            oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
 
284
            oauth_version="1.0",
 
285
        )
 
286
        signature = _oauth_signature(consumer_token, "GET", url, args,
 
287
                                     request_token)
 
288
        args["oauth_signature"] = signature
 
289
        return url + "?" + urllib.urlencode(args)
 
290
 
 
291
    def _on_access_token(self, callback, response):
 
292
        if response.error:
 
293
            _log.warning("Could not fetch access token")
 
294
            callback(None)
 
295
            return
 
296
        access_token = _oauth_parse_response(response.body)
 
297
        user = self._oauth_get_user(access_token, self.async_callback(
 
298
             self._on_oauth_get_user, access_token, callback))
 
299
 
 
300
    def _oauth_get_user(self, access_token, callback):
 
301
        raise NotImplementedError()
 
302
 
 
303
    def _on_oauth_get_user(self, access_token, callback, user):
 
304
        if not user:
 
305
            callback(None)
 
306
            return
 
307
        user["access_token"] = access_token
 
308
        callback(user)
 
309
 
 
310
    def _oauth_request_parameters(self, url, access_token, parameters={},
 
311
                                  method="GET"):
 
312
        """Returns the OAuth parameters as a dict for the given request.
 
313
 
 
314
        parameters should include all POST arguments and query string arguments
 
315
        that will be sent with the request.
 
316
        """
 
317
        consumer_token = self._oauth_consumer_token()
 
318
        base_args = dict(
 
319
            oauth_consumer_key=consumer_token["key"],
 
320
            oauth_token=access_token["key"],
 
321
            oauth_signature_method="HMAC-SHA1",
 
322
            oauth_timestamp=str(int(time.time())),
 
323
            oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
 
324
            oauth_version="1.0",
 
325
        )
 
326
        args = {}
 
327
        args.update(base_args)
 
328
        args.update(parameters)
 
329
        signature = _oauth_signature(consumer_token, method, url, args,
 
330
                                     access_token)
 
331
        base_args["oauth_signature"] = signature
 
332
        return base_args
 
333
 
 
334
 
 
335
class TwitterMixin(OAuthMixin):
 
336
    """Twitter OAuth authentication.
 
337
 
 
338
    To authenticate with Twitter, register your application with
 
339
    Twitter at http://twitter.com/apps. Then copy your Consumer Key and
 
340
    Consumer Secret to the application settings 'twitter_consumer_key' and
 
341
    'twitter_consumer_secret'. Use this Mixin on the handler for the URL
 
342
    you registered as your application's Callback URL.
 
343
 
 
344
    When your application is set up, you can use this Mixin like this
 
345
    to authenticate the user with Twitter and get access to their stream:
 
346
 
 
347
    class TwitterHandler(tornado.web.RequestHandler,
 
348
                         tornado.auth.TwitterMixin):
 
349
        @tornado.web.asynchronous
 
350
        def get(self):
 
351
            if self.get_argument("oauth_token", None):
 
352
                self.get_authenticated_user(self.async_callback(self._on_auth))
 
353
                return
 
354
            self.authorize_redirect()
 
355
    
 
356
        def _on_auth(self, user):
 
357
            if not user:
 
358
                raise tornado.web.HTTPError(500, "Twitter auth failed")
 
359
            # Save the user using, e.g., set_secure_cookie()
 
360
 
 
361
    The user object returned by get_authenticated_user() includes the
 
362
    attributes 'username', 'name', and all of the custom Twitter user
 
363
    attributes describe at
 
364
    http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
 
365
    in addition to 'access_token'. You should save the access token with
 
366
    the user; it is required to make requests on behalf of the user later
 
367
    with twitter_request().
 
368
    """
 
369
    _OAUTH_REQUEST_TOKEN_URL = "http://twitter.com/oauth/request_token"
 
370
    _OAUTH_ACCESS_TOKEN_URL = "http://twitter.com/oauth/access_token"
 
371
    _OAUTH_AUTHORIZE_URL = "http://twitter.com/oauth/authorize"
 
372
    _OAUTH_AUTHENTICATE_URL = "http://twitter.com/oauth/authenticate"
 
373
    _OAUTH_NO_CALLBACKS = True
 
374
 
 
375
    def authenticate_redirect(self):
 
376
        """Just like authorize_redirect(), but auto-redirects if authorized.
 
377
 
 
378
        This is generally the right interface to use if you are using
 
379
        Twitter for single-sign on.
 
380
        """
 
381
        http = httpclient.AsyncHTTPClient()
 
382
        http.fetch(self._oauth_request_token_url(), self.async_callback(
 
383
            self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
 
384
 
 
385
    def twitter_request(self, path, callback, access_token=None,
 
386
                           post_args=None, **args):
 
387
        """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
 
388
 
 
389
        The path should not include the format (we automatically append
 
390
        ".json" and parse the JSON output).
 
391
 
 
392
        If the request is a POST, post_args should be provided. Query
 
393
        string arguments should be given as keyword arguments.
 
394
 
 
395
        All the Twitter methods are documented at
 
396
        http://apiwiki.twitter.com/Twitter-API-Documentation.
 
397
 
 
398
        Many methods require an OAuth access token which you can obtain
 
399
        through authorize_redirect() and get_authenticated_user(). The
 
400
        user returned through that process includes an 'access_token'
 
401
        attribute that can be used to make authenticated requests via
 
402
        this method. Example usage:
 
403
 
 
404
        class MainHandler(tornado.web.RequestHandler,
 
405
                          tornado.auth.TwitterMixin):
 
406
            @tornado.web.authenticated
 
407
            @tornado.web.asynchronous
 
408
            def get(self):
 
409
                self.twitter_request(
 
410
                    "/statuses/update",
 
411
                    post_args={"status": "Testing Tornado Web Server"},
 
412
                    access_token=user["access_token"],
 
413
                    callback=self.async_callback(self._on_post))
 
414
 
 
415
            def _on_post(self, new_entry):
 
416
                if not new_entry:
 
417
                    # Call failed; perhaps missing permission?
 
418
                    self.authorize_redirect()
 
419
                    return
 
420
                self.finish("Posted a message!")
 
421
 
 
422
        """
 
423
        # Add the OAuth resource request signature if we have credentials
 
424
        url = "http://twitter.com" + path + ".json"
 
425
        if access_token:
 
426
            all_args = {}
 
427
            all_args.update(args)
 
428
            all_args.update(post_args or {})
 
429
            consumer_token = self._oauth_consumer_token()
 
430
            method = "POST" if post_args is not None else "GET"
 
431
            oauth = self._oauth_request_parameters(
 
432
                url, access_token, all_args, method=method)
 
433
            args.update(oauth)
 
434
        if args: url += "?" + urllib.urlencode(args)
 
435
        callback = self.async_callback(self._on_twitter_request, callback)
 
436
        http = httpclient.AsyncHTTPClient()
 
437
        if post_args is not None:
 
438
            http.fetch(url, method="POST", body=urllib.urlencode(post_args),
 
439
                       callback=callback)
 
440
        else:
 
441
            http.fetch(url, callback=callback)
 
442
    
 
443
    def _on_twitter_request(self, callback, response):
 
444
        if response.error:
 
445
            _log.warning("Error response %s fetching %s", response.error,
 
446
                            response.request.url)
 
447
            callback(None)
 
448
            return
 
449
        callback(escape.json_decode(response.body))
 
450
 
 
451
    def _oauth_consumer_token(self):
 
452
        self.require_setting("twitter_consumer_key", "Twitter OAuth")
 
453
        self.require_setting("twitter_consumer_secret", "Twitter OAuth")
 
454
        return dict(
 
455
            key=self.settings["twitter_consumer_key"],
 
456
            secret=self.settings["twitter_consumer_secret"])
 
457
 
 
458
    def _oauth_get_user(self, access_token, callback):
 
459
        callback = self.async_callback(self._parse_user_response, callback)
 
460
        self.twitter_request(
 
461
            "/users/show/" + access_token["screen_name"],
 
462
            access_token=access_token, callback=callback)
 
463
 
 
464
    def _parse_user_response(self, callback, user):
 
465
        if user:
 
466
            user["username"] = user["screen_name"]
 
467
        callback(user)
 
468
 
 
469
 
 
470
class FriendFeedMixin(OAuthMixin):
 
471
    """FriendFeed OAuth authentication.
 
472
 
 
473
    To authenticate with FriendFeed, register your application with
 
474
    FriendFeed at http://friendfeed.com/api/applications. Then
 
475
    copy your Consumer Key and Consumer Secret to the application settings
 
476
    'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use
 
477
    this Mixin on the handler for the URL you registered as your
 
478
    application's Callback URL.
 
479
 
 
480
    When your application is set up, you can use this Mixin like this
 
481
    to authenticate the user with FriendFeed and get access to their feed:
 
482
 
 
483
    class FriendFeedHandler(tornado.web.RequestHandler,
 
484
                            tornado.auth.FriendFeedMixin):
 
485
        @tornado.web.asynchronous
 
486
        def get(self):
 
487
            if self.get_argument("oauth_token", None):
 
488
                self.get_authenticated_user(self.async_callback(self._on_auth))
 
489
                return
 
490
            self.authorize_redirect()
 
491
    
 
492
        def _on_auth(self, user):
 
493
            if not user:
 
494
                raise tornado.web.HTTPError(500, "FriendFeed auth failed")
 
495
            # Save the user using, e.g., set_secure_cookie()
 
496
 
 
497
    The user object returned by get_authenticated_user() includes the
 
498
    attributes 'username', 'name', and 'description' in addition to
 
499
    'access_token'. You should save the access token with the user;
 
500
    it is required to make requests on behalf of the user later with
 
501
    friendfeed_request().
 
502
    """
 
503
    _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
 
504
    _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
 
505
    _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
 
506
    _OAUTH_NO_CALLBACKS = True
 
507
 
 
508
    def friendfeed_request(self, path, callback, access_token=None,
 
509
                           post_args=None, **args):
 
510
        """Fetches the given relative API path, e.g., "/bret/friends"
 
511
 
 
512
        If the request is a POST, post_args should be provided. Query
 
513
        string arguments should be given as keyword arguments.
 
514
 
 
515
        All the FriendFeed methods are documented at
 
516
        http://friendfeed.com/api/documentation.
 
517
 
 
518
        Many methods require an OAuth access token which you can obtain
 
519
        through authorize_redirect() and get_authenticated_user(). The
 
520
        user returned through that process includes an 'access_token'
 
521
        attribute that can be used to make authenticated requests via
 
522
        this method. Example usage:
 
523
 
 
524
        class MainHandler(tornado.web.RequestHandler,
 
525
                          tornado.auth.FriendFeedMixin):
 
526
            @tornado.web.authenticated
 
527
            @tornado.web.asynchronous
 
528
            def get(self):
 
529
                self.friendfeed_request(
 
530
                    "/entry",
 
531
                    post_args={"body": "Testing Tornado Web Server"},
 
532
                    access_token=self.current_user["access_token"],
 
533
                    callback=self.async_callback(self._on_post))
 
534
 
 
535
            def _on_post(self, new_entry):
 
536
                if not new_entry:
 
537
                    # Call failed; perhaps missing permission?
 
538
                    self.authorize_redirect()
 
539
                    return
 
540
                self.finish("Posted a message!")
 
541
 
 
542
        """
 
543
        # Add the OAuth resource request signature if we have credentials
 
544
        url = "http://friendfeed-api.com/v2" + path
 
545
        if access_token:
 
546
            all_args = {}
 
547
            all_args.update(args)
 
548
            all_args.update(post_args or {})
 
549
            consumer_token = self._oauth_consumer_token()
 
550
            method = "POST" if post_args is not None else "GET"
 
551
            oauth = self._oauth_request_parameters(
 
552
                url, access_token, all_args, method=method)
 
553
            args.update(oauth)
 
554
        if args: url += "?" + urllib.urlencode(args)
 
555
        callback = self.async_callback(self._on_friendfeed_request, callback)
 
556
        http = httpclient.AsyncHTTPClient()
 
557
        if post_args is not None:
 
558
            http.fetch(url, method="POST", body=urllib.urlencode(post_args),
 
559
                       callback=callback)
 
560
        else:
 
561
            http.fetch(url, callback=callback)
 
562
    
 
563
    def _on_friendfeed_request(self, callback, response):
 
564
        if response.error:
 
565
            _log.warning("Error response %s fetching %s", response.error,
 
566
                            response.request.url)
 
567
            callback(None)
 
568
            return
 
569
        callback(escape.json_decode(response.body))
 
570
 
 
571
    def _oauth_consumer_token(self):
 
572
        self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
 
573
        self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
 
574
        return dict(
 
575
            key=self.settings["friendfeed_consumer_key"],
 
576
            secret=self.settings["friendfeed_consumer_secret"])
 
577
 
 
578
    def _oauth_get_user(self, access_token, callback):
 
579
        callback = self.async_callback(self._parse_user_response, callback)
 
580
        self.friendfeed_request(
 
581
            "/feedinfo/" + access_token["username"],
 
582
            include="id,name,description", access_token=access_token,
 
583
            callback=callback)
 
584
 
 
585
    def _parse_user_response(self, callback, user):
 
586
        if user:
 
587
            user["username"] = user["id"]
 
588
        callback(user)
 
589
 
 
590
 
 
591
class GoogleMixin(OpenIdMixin, OAuthMixin):
 
592
    """Google Open ID / OAuth authentication.
 
593
 
 
594
    No application registration is necessary to use Google for authentication
 
595
    or to access Google resources on behalf of a user. To authenticate with
 
596
    Google, redirect with authenticate_redirect(). On return, parse the
 
597
    response with get_authenticated_user(). We send a dict containing the
 
598
    values for the user, including 'email', 'name', and 'locale'.
 
599
    Example usage:
 
600
 
 
601
    class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
 
602
       @tornado.web.asynchronous
 
603
       def get(self):
 
604
           if self.get_argument("openid.mode", None):
 
605
               self.get_authenticated_user(self.async_callback(self._on_auth))
 
606
               return
 
607
        self.authenticate_redirect()
 
608
    
 
609
        def _on_auth(self, user):
 
610
            if not user:
 
611
                raise tornado.web.HTTPError(500, "Google auth failed")
 
612
            # Save the user with, e.g., set_secure_cookie()
 
613
 
 
614
    """
 
615
    _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
 
616
    _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
 
617
 
 
618
    def authorize_redirect(self, oauth_scope, callback_uri=None,
 
619
                           ax_attrs=["name","email","language","username"]):
 
620
        """Authenticates and authorizes for the given Google resource.
 
621
 
 
622
        Some of the available resources are:
 
623
 
 
624
           Gmail Contacts - http://www.google.com/m8/feeds/
 
625
           Calendar - http://www.google.com/calendar/feeds/
 
626
           Finance - http://finance.google.com/finance/feeds/
 
627
 
 
628
        You can authorize multiple resources by separating the resource
 
629
        URLs with a space.
 
630
        """
 
631
        callback_uri = callback_uri or self.request.path
 
632
        args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
 
633
                                 oauth_scope=oauth_scope)
 
634
        self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
 
635
 
 
636
    def get_authenticated_user(self, callback):
 
637
        """Fetches the authenticated user data upon redirect."""
 
638
        # Look to see if we are doing combined OpenID/OAuth
 
639
        oauth_ns = ""
 
640
        for name, values in self.request.arguments.iteritems():
 
641
            if name.startswith("openid.ns.") and \
 
642
               values[-1] == u"http://specs.openid.net/extensions/oauth/1.0":
 
643
                oauth_ns = name[10:]
 
644
                break
 
645
        token = self.get_argument("openid." + oauth_ns + ".request_token", "")
 
646
        if token:
 
647
            http = httpclient.AsyncHTTPClient()
 
648
            token = dict(key=token, secret="")
 
649
            http.fetch(self._oauth_access_token_url(token),
 
650
                       self.async_callback(self._on_access_token, callback))
 
651
        else:
 
652
            OpenIdMixin.get_authenticated_user(self, callback)
 
653
 
 
654
    def _oauth_consumer_token(self):
 
655
        self.require_setting("google_consumer_key", "Google OAuth")
 
656
        self.require_setting("google_consumer_secret", "Google OAuth")
 
657
        return dict(
 
658
            key=self.settings["google_consumer_key"],
 
659
            secret=self.settings["google_consumer_secret"])
 
660
 
 
661
    def _oauth_get_user(self, access_token, callback):
 
662
        OpenIdMixin.get_authenticated_user(self, callback)
 
663
 
 
664
 
 
665
class FacebookMixin(object):
 
666
    """Facebook Connect authentication.
 
667
 
 
668
    To authenticate with Facebook, register your application with
 
669
    Facebook at http://www.facebook.com/developers/apps.php. Then
 
670
    copy your API Key and Application Secret to the application settings
 
671
    'facebook_api_key' and 'facebook_secret'.
 
672
 
 
673
    When your application is set up, you can use this Mixin like this
 
674
    to authenticate the user with Facebook:
 
675
 
 
676
    class FacebookHandler(tornado.web.RequestHandler,
 
677
                          tornado.auth.FacebookMixin):
 
678
        @tornado.web.asynchronous
 
679
        def get(self):
 
680
            if self.get_argument("session", None):
 
681
                self.get_authenticated_user(self.async_callback(self._on_auth))
 
682
                return
 
683
            self.authenticate_redirect()
 
684
    
 
685
        def _on_auth(self, user):
 
686
            if not user:
 
687
                raise tornado.web.HTTPError(500, "Facebook auth failed")
 
688
            # Save the user using, e.g., set_secure_cookie()
 
689
 
 
690
    The user object returned by get_authenticated_user() includes the
 
691
    attributes 'facebook_uid' and 'name' in addition to session attributes
 
692
    like 'session_key'. You should save the session key with the user; it is
 
693
    required to make requests on behalf of the user later with
 
694
    facebook_request().
 
695
    """
 
696
    def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
 
697
                              extended_permissions=None):
 
698
        """Authenticates/installs this app for the current user."""
 
699
        self.require_setting("facebook_api_key", "Facebook Connect")
 
700
        callback_uri = callback_uri or self.request.path
 
701
        args = {
 
702
            "api_key": self.settings["facebook_api_key"],
 
703
            "v": "1.0",
 
704
            "fbconnect": "true",
 
705
            "display": "page",
 
706
            "next": urlparse.urljoin(self.request.full_url(), callback_uri),
 
707
            "return_session": "true",
 
708
        }
 
709
        if cancel_uri:
 
710
            args["cancel_url"] = urlparse.urljoin(
 
711
                self.request.full_url(), cancel_uri)
 
712
        if extended_permissions:
 
713
            if isinstance(extended_permissions, basestring):
 
714
                extended_permissions = [extended_permissions]
 
715
            args["req_perms"] = ",".join(extended_permissions)
 
716
        self.redirect("http://www.facebook.com/login.php?" +
 
717
                      urllib.urlencode(args))
 
718
 
 
719
    def authorize_redirect(self, extended_permissions, callback_uri=None,
 
720
                           cancel_uri=None):
 
721
        """Redirects to an authorization request for the given FB resource.
 
722
 
 
723
        The available resource names are listed at
 
724
        http://wiki.developers.facebook.com/index.php/Extended_permission.
 
725
        The most common resource types include:
 
726
 
 
727
            publish_stream
 
728
            read_stream
 
729
            email
 
730
            sms
 
731
 
 
732
        extended_permissions can be a single permission name or a list of
 
733
        names. To get the session secret and session key, call
 
734
        get_authenticated_user() just as you would with
 
735
        authenticate_redirect().
 
736
        """
 
737
        self.authenticate_redirect(callback_uri, cancel_uri,
 
738
                                   extended_permissions)
 
739
 
 
740
    def get_authenticated_user(self, callback):
 
741
        """Fetches the authenticated Facebook user.
 
742
 
 
743
        The authenticated user includes the special Facebook attributes
 
744
        'session_key' and 'facebook_uid' in addition to the standard
 
745
        user attributes like 'name'.
 
746
        """
 
747
        self.require_setting("facebook_api_key", "Facebook Connect")
 
748
        session = escape.json_decode(self.get_argument("session"))
 
749
        self.facebook_request(
 
750
            method="facebook.users.getInfo",
 
751
            callback=self.async_callback(
 
752
                self._on_get_user_info, callback, session),
 
753
            session_key=session["session_key"],
 
754
            uids=session["uid"],
 
755
            fields="uid,first_name,last_name,name,locale,pic_square," \
 
756
                   "profile_url,username")
 
757
 
 
758
    def facebook_request(self, method, callback, **args):
 
759
        """Makes a Facebook API REST request.
 
760
 
 
761
        We automatically include the Facebook API key and signature, but
 
762
        it is the callers responsibility to include 'session_key' and any
 
763
        other required arguments to the method.
 
764
 
 
765
        The available Facebook methods are documented here:
 
766
        http://wiki.developers.facebook.com/index.php/API
 
767
 
 
768
        Here is an example for the stream.get() method:
 
769
 
 
770
        class MainHandler(tornado.web.RequestHandler,
 
771
                          tornado.auth.FacebookMixin):
 
772
            @tornado.web.authenticated
 
773
            @tornado.web.asynchronous
 
774
            def get(self):
 
775
                self.facebook_request(
 
776
                    method="stream.get",
 
777
                    callback=self.async_callback(self._on_stream),
 
778
                    session_key=self.current_user["session_key"])
 
779
 
 
780
            def _on_stream(self, stream):
 
781
                if stream is None:
 
782
                   # Not authorized to read the stream yet?
 
783
                   self.redirect(self.authorize_redirect("read_stream"))
 
784
                   return
 
785
                self.render("stream.html", stream=stream)
 
786
 
 
787
        """
 
788
        self.require_setting("facebook_api_key", "Facebook Connect")
 
789
        self.require_setting("facebook_secret", "Facebook Connect")
 
790
        if not method.startswith("facebook."):
 
791
            method = "facebook." + method
 
792
        args["api_key"] = self.settings["facebook_api_key"]
 
793
        args["v"] = "1.0"
 
794
        args["method"] = method
 
795
        args["call_id"] = str(long(time.time() * 1e6))
 
796
        args["format"] = "json"
 
797
        args["sig"] = self._signature(args)
 
798
        url = "http://api.facebook.com/restserver.php?" + \
 
799
            urllib.urlencode(args)
 
800
        http = httpclient.AsyncHTTPClient()
 
801
        http.fetch(url, callback=self.async_callback(
 
802
            self._parse_response, callback))
 
803
 
 
804
    def _on_get_user_info(self, callback, session, users):
 
805
        if users is None:
 
806
            callback(None)
 
807
            return
 
808
        callback({
 
809
            "name": users[0]["name"],
 
810
            "first_name": users[0]["first_name"],
 
811
            "last_name": users[0]["last_name"],
 
812
            "uid": users[0]["uid"],
 
813
            "locale": users[0]["locale"],
 
814
            "pic_square": users[0]["pic_square"],
 
815
            "profile_url": users[0]["profile_url"],
 
816
            "username": users[0].get("username"),
 
817
            "session_key": session["session_key"],
 
818
            "session_expires": session["expires"],
 
819
        })
 
820
 
 
821
    def _parse_response(self, callback, response):
 
822
        if response.error:
 
823
            _log.warning("HTTP error from Facebook: %s", response.error)
 
824
            callback(None)
 
825
            return
 
826
        try:
 
827
            json = escape.json_decode(response.body)
 
828
        except:
 
829
            _log.warning("Invalid JSON from Facebook: %r", response.body)
 
830
            callback(None)
 
831
            return
 
832
        if isinstance(json, dict) and json.get("error_code"):
 
833
            _log.warning("Facebook error: %d: %r", json["error_code"],
 
834
                            json.get("error_msg"))
 
835
            callback(None)
 
836
            return
 
837
        callback(json)
 
838
 
 
839
    def _signature(self, args):
 
840
        parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
 
841
        body = "".join(parts) + self.settings["facebook_secret"]
 
842
        if isinstance(body, unicode): body = body.encode("utf-8")
 
843
        return hashlib.md5(body).hexdigest()
 
844
 
 
845
 
 
846
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
 
847
    """Calculates the HMAC-SHA1 OAuth signature for the given request.
 
848
 
 
849
    See http://oauth.net/core/1.0/#signing_process
 
850
    """
 
851
    parts = urlparse.urlparse(url)
 
852
    scheme, netloc, path = parts[:3]
 
853
    normalized_url = scheme.lower() + "://" + netloc.lower() + path
 
854
 
 
855
    base_elems = []
 
856
    base_elems.append(method.upper())
 
857
    base_elems.append(normalized_url)
 
858
    base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
 
859
                               for k, v in sorted(parameters.items())))
 
860
    base_string =  "&".join(_oauth_escape(e) for e in base_elems)
 
861
 
 
862
    key_elems = [consumer_token["secret"]]
 
863
    key_elems.append(token["secret"] if token else "")
 
864
    key = "&".join(key_elems)
 
865
 
 
866
    hash = hmac.new(key, base_string, hashlib.sha1)
 
867
    return binascii.b2a_base64(hash.digest())[:-1]
 
868
 
 
869
 
 
870
def _oauth_escape(val):
 
871
    if isinstance(val, unicode):
 
872
        val = val.encode("utf-8")
 
873
    return urllib.quote(val, safe="~")
 
874
 
 
875
 
 
876
def _oauth_parse_response(body):
 
877
    p = cgi.parse_qs(body, keep_blank_values=False)
 
878
    token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0])
 
879
 
 
880
    # Add the extra parameters the Provider included to the token
 
881
    special = ("oauth_token", "oauth_token_secret")
 
882
    token.update((k, p[k][0]) for k in p if k not in special)
 
883
    return token