##// END OF EJS Templates
authomatic: fixed oauth data types
super-admin -
r5114:6bd1539d default
parent child Browse files
Show More
@@ -1,281 +1,280 b''
1 1
2 2 """
3 3 Adapters
4 4 --------
5 5
6 6 .. contents::
7 7 :backlinks: none
8 8
9 9 The :func:`authomatic.login` function needs access to functionality like
10 10 getting the **URL** of the handler where it is being called, getting the
11 11 **request params** and **cookies** and **writing the body**, **headers**
12 12 and **status** to the response.
13 13
14 14 Since implementation of these features varies across Python web frameworks,
15 15 the Authomatic library uses **adapters** to unify these differences into a
16 16 single interface.
17 17
18 18 Available Adapters
19 19 ^^^^^^^^^^^^^^^^^^
20 20
21 21 If you are missing an adapter for the framework of your choice, please
22 22 open an `enhancement issue <https://github.com/authomatic/authomatic/issues>`_
23 23 or consider a contribution to this module by
24 24 :ref:`implementing <implement_adapters>` one by yourself.
25 25 Its very easy and shouldn't take you more than a few minutes.
26 26
27 27 .. autoclass:: DjangoAdapter
28 28 :members:
29 29
30 30 .. autoclass:: Webapp2Adapter
31 31 :members:
32 32
33 33 .. autoclass:: WebObAdapter
34 34 :members:
35 35
36 36 .. autoclass:: WerkzeugAdapter
37 37 :members:
38 38
39 39 .. _implement_adapters:
40 40
41 41 Implementing an Adapter
42 42 ^^^^^^^^^^^^^^^^^^^^^^^
43 43
44 44 Implementing an adapter for a Python web framework is pretty easy.
45 45
46 46 Do it by subclassing the :class:`.BaseAdapter` abstract class.
47 47 There are only **six** members that you need to implement.
48 48
49 49 Moreover if your framework is based on the |webob|_ or |werkzeug|_ package
50 50 you can subclass the :class:`.WebObAdapter` or :class:`.WerkzeugAdapter`
51 51 respectively.
52 52
53 53 .. autoclass:: BaseAdapter
54 54 :members:
55 55
56 56 """
57 57
58 58 import abc
59 59 from authomatic.core import Response
60 60
61 61
62 62 class BaseAdapter(object, metaclass=abc.ABCMeta):
63 63 """
64 64 Base class for platform adapters.
65 65
66 66 Defines common interface for WSGI framework specific functionality.
67 67
68 68 """
69 69
70 70 @abc.abstractproperty
71 71 def params(self):
72 72 """
73 73 Must return a :class:`dict` of all request parameters of any HTTP
74 74 method.
75 75
76 76 :returns:
77 77 :class:`dict`
78 78
79 79 """
80 80
81 81 @abc.abstractproperty
82 82 def url(self):
83 83 """
84 84 Must return the url of the actual request including path but without
85 85 query and fragment.
86 86
87 87 :returns:
88 88 :class:`str`
89 89
90 90 """
91 91
92 92 @abc.abstractproperty
93 93 def cookies(self):
94 94 """
95 95 Must return cookies as a :class:`dict`.
96 96
97 97 :returns:
98 98 :class:`dict`
99 99
100 100 """
101 101
102 102 @abc.abstractmethod
103 103 def write(self, value):
104 104 """
105 105 Must write specified value to response.
106 106
107 107 :param str value:
108 108 String to be written to response.
109 109
110 110 """
111 111
112 112 @abc.abstractmethod
113 113 def set_header(self, key, value):
114 114 """
115 115 Must set response headers to ``Key: value``.
116 116
117 117 :param str key:
118 118 Header name.
119 119
120 120 :param str value:
121 121 Header value.
122 122
123 123 """
124 124
125 125 @abc.abstractmethod
126 126 def set_status(self, status):
127 127 """
128 128 Must set the response status e.g. ``'302 Found'``.
129 129
130 130 :param str status:
131 131 The HTTP response status.
132 132
133 133 """
134 134
135 135
136 136 class DjangoAdapter(BaseAdapter):
137 137 """
138 138 Adapter for the |django|_ framework.
139 139 """
140 140
141 141 def __init__(self, request, response):
142 142 """
143 143 :param request:
144 144 An instance of the :class:`django.http.HttpRequest` class.
145 145
146 146 :param response:
147 147 An instance of the :class:`django.http.HttpResponse` class.
148 148 """
149 149 self.request = request
150 150 self.response = response
151 151
152 152 @property
153 153 def params(self):
154 154 params = {}
155 155 params.update(self.request.GET.dict())
156 156 params.update(self.request.POST.dict())
157 157 return params
158 158
159 159 @property
160 160 def url(self):
161 161 return self.request.build_absolute_uri(self.request.path)
162 162
163 163 @property
164 164 def cookies(self):
165 165 return dict(self.request.COOKIES)
166 166
167 167 def write(self, value):
168 168 self.response.write(value)
169 169
170 170 def set_header(self, key, value):
171 171 self.response[key] = value
172 172
173 173 def set_status(self, status):
174 174 status_code, reason = status.split(' ', 1)
175 175 self.response.status_code = int(status_code)
176 176
177 177
178 178 class WebObAdapter(BaseAdapter):
179 179 """
180 180 Adapter for the |webob|_ package.
181 181 """
182 182
183 183 def __init__(self, request, response):
184 184 """
185 185 :param request:
186 186 A |webob|_ :class:`Request` instance.
187 187
188 188 :param response:
189 189 A |webob|_ :class:`Response` instance.
190 190 """
191 191 self.request = request
192 192 self.response = response
193 193
194 194 # =========================================================================
195 195 # Request
196 196 # =========================================================================
197 197
198 198 @property
199 199 def url(self):
200 200 return self.request.path_url
201 201
202 202 @property
203 203 def params(self):
204 204 return dict(self.request.params)
205 205
206 206 @property
207 207 def cookies(self):
208 208 return dict(self.request.cookies)
209 209
210 210 # =========================================================================
211 211 # Response
212 212 # =========================================================================
213 213
214 214 def write(self, value):
215 215 self.response.write(value)
216 216
217 217 def set_header(self, key, value):
218 218 self.response.headers[key] = str(value)
219 219
220 220 def set_status(self, status):
221 221 self.response.status = status
222 222
223 223
224 224 class Webapp2Adapter(WebObAdapter):
225 225 """
226 226 Adapter for the |webapp2|_ framework.
227 227
228 228 Inherits from the :class:`.WebObAdapter`.
229 229
230 230 """
231 231
232 232 def __init__(self, handler):
233 233 """
234 234 :param handler:
235 235 A :class:`webapp2.RequestHandler` instance.
236 236 """
237 237 self.request = handler.request
238 238 self.response = handler.response
239 239
240 240
241 241 class WerkzeugAdapter(BaseAdapter):
242 242 """
243 243 Adapter for |flask|_ and other |werkzeug|_ based frameworks.
244 244
245 245 Thanks to `Mark Steve Samson <http://marksteve.com>`_.
246 246
247 247 """
248 248
249 249 @property
250 250 def params(self):
251 251 return self.request.args
252 252
253 253 @property
254 254 def url(self):
255 255 return self.request.base_url
256 256
257 257 @property
258 258 def cookies(self):
259 259 return self.request.cookies
260 260
261 261 def __init__(self, request, response):
262 262 """
263 263 :param request:
264 264 Instance of the :class:`werkzeug.wrappers.Request` class.
265 265
266 266 :param response:
267 267 Instance of the :class:`werkzeug.wrappers.Response` class.
268 268 """
269 269
270 270 self.request = request
271 271 self.response = response
272 272
273 273 def write(self, value):
274 #self.response.data = self.response.data.decode('utf-8') + value
275 self.response.data = self.response.data + value
274 self.response.data = self.response.data.decode('utf-8') + value
276 275
277 276 def set_header(self, key, value):
278 277 self.response.headers[key] = value
279 278
280 279 def set_status(self, status):
281 280 self.response.status = status
@@ -1,1765 +1,1764 b''
1 1
2 2
3 3 import collections
4 4 import copy
5 5 import datetime
6 6 import hashlib
7 7 import hmac
8 8 import json
9 9 import logging
10 10 try:
11 11 import cPickle as pickle
12 12 except ImportError:
13 13 import pickle
14 14 import sys
15 15 import threading
16 16 import time
17 17 from xml.etree import ElementTree
18 18
19 19 from authomatic.exceptions import (
20 20 ConfigError,
21 21 CredentialsError,
22 22 ImportStringError,
23 23 RequestElementsError,
24 24 SessionError,
25 25 )
26 26 from authomatic import six
27 27 from authomatic.six.moves import urllib_parse as parse
28 28
29 29
30 30 # =========================================================================
31 31 # Global variables !!!
32 32 # =========================================================================
33 33
34 34 _logger = logging.getLogger(__name__)
35 35 _logger.addHandler(logging.StreamHandler(sys.stdout))
36 36
37 37 _counter = None
38 38
39 39
40 40 def normalize_dict(dict_):
41 41 """
42 42 Replaces all values that are single-item iterables with the value of its
43 43 index 0.
44 44
45 45 :param dict dict_:
46 46 Dictionary to normalize.
47 47
48 48 :returns:
49 49 Normalized dictionary.
50 50
51 51 """
52 52
53 53 return dict([(k, v[0] if not isinstance(v, str) and len(v) == 1 else v)
54 54 for k, v in list(dict_.items())])
55 55
56 56
57 57 def items_to_dict(items):
58 58 """
59 59 Converts list of tuples to dictionary with duplicate keys converted to
60 60 lists.
61 61
62 62 :param list items:
63 63 List of tuples.
64 64
65 65 :returns:
66 66 :class:`dict`
67 67
68 68 """
69 69
70 70 res = collections.defaultdict(list)
71 71
72 72 for k, v in items:
73 73 res[k].append(v)
74 74
75 75 return normalize_dict(dict(res))
76 76
77 77
78 78 class Counter(object):
79 79 """
80 80 A simple counter to be used in the config to generate unique `id` values.
81 81 """
82 82
83 83 def __init__(self, start=0):
84 84 self._count = start
85 85
86 86 def count(self):
87 87 self._count += 1
88 88 return self._count
89 89
90 90
91 91 _counter = Counter()
92 92
93 93
94 94 def provider_id():
95 95 """
96 96 A simple counter to be used in the config to generate unique `IDs`.
97 97
98 98 :returns:
99 99 :class:`int`.
100 100
101 101 Use it in the :doc:`config` like this:
102 102 ::
103 103
104 104 import authomatic
105 105
106 106 CONFIG = {
107 107 'facebook': {
108 108 'class_': authomatic.providers.oauth2.Facebook,
109 109 'id': authomatic.provider_id(), # returns 1
110 110 'consumer_key': '##########',
111 111 'consumer_secret': '##########',
112 112 'scope': ['user_about_me', 'email']
113 113 },
114 114 'google': {
115 115 'class_': 'authomatic.providers.oauth2.Google',
116 116 'id': authomatic.provider_id(), # returns 2
117 117 'consumer_key': '##########',
118 118 'consumer_secret': '##########',
119 119 'scope': ['https://www.googleapis.com/auth/userinfo.profile',
120 120 'https://www.googleapis.com/auth/userinfo.email']
121 121 },
122 122 'windows_live': {
123 123 'class_': 'oauth2.WindowsLive',
124 124 'id': authomatic.provider_id(), # returns 3
125 125 'consumer_key': '##########',
126 126 'consumer_secret': '##########',
127 127 'scope': ['wl.basic', 'wl.emails', 'wl.photos']
128 128 },
129 129 }
130 130
131 131 """
132 132
133 133 return _counter.count()
134 134
135 135
136 136 def escape(s):
137 137 """
138 138 Escape a URL including any /.
139 139 """
140 140 return parse.quote(s.encode('utf-8'), safe='~')
141 141
142 142
143 143 def json_qs_parser(body):
144 144 """
145 145 Parses response body from JSON, XML or query string.
146 146
147 147 :param body:
148 148 string
149 149
150 150 :returns:
151 151 :class:`dict`, :class:`list` if input is JSON or query string,
152 152 :class:`xml.etree.ElementTree.Element` if XML.
153 153
154 154 """
155 155 try:
156 156 # Try JSON first.
157 157 return json.loads(body)
158 158 except (OverflowError, TypeError, ValueError):
159 159 pass
160 160
161 161 try:
162 162 # Then XML.
163 163 return ElementTree.fromstring(body)
164 164 except (ElementTree.ParseError, TypeError, ValueError):
165 165 pass
166 166
167 167 # Finally query string.
168 168 return dict(parse.parse_qsl(body))
169 169
170 170
171 171 def import_string(import_name, silent=False):
172 172 """
173 173 Imports an object by string in dotted notation.
174 174
175 175 taken `from webapp2.import_string() <http://webapp-
176 176 improved.appspot.com/api/webapp2.html#webapp2.import_string>`_
177 177
178 178 """
179 179
180 180 try:
181 181 if '.' in import_name:
182 182 module, obj = import_name.rsplit('.', 1)
183 183 return getattr(__import__(module, None, None, [obj]), obj)
184 184 else:
185 185 return __import__(import_name)
186 186 except (ImportError, AttributeError) as e:
187 187 if not silent:
188 188 raise ImportStringError('Import from string failed for path {0}'
189 189 .format(import_name), str(e))
190 190
191 191
192 192 def resolve_provider_class(class_):
193 193 """
194 194 Returns a provider class.
195 195
196 196 :param class_name: :class:`string` or
197 197 :class:`authomatic.providers.BaseProvider` subclass.
198 198
199 199 """
200 200
201 201 if isinstance(class_, str):
202 202 # prepare path for authomatic.providers package
203 203 path = '.'.join([__package__, 'providers', class_])
204 204
205 205 # try to import class by string from providers module or by fully
206 206 # qualified path
207 207 return import_string(class_, True) or import_string(path)
208 208 else:
209 209 return class_
210 210
211 211
212 212 def id_to_name(config, short_name):
213 213 """
214 214 Returns the provider :doc:`config` key based on it's ``id`` value.
215 215
216 216 :param dict config:
217 217 :doc:`config`.
218 218 :param id:
219 219 Value of the id parameter in the :ref:`config` to search for.
220 220
221 221 """
222 222
223 223 for k, v in list(config.items()):
224 224 if v.get('id') == short_name:
225 225 return k
226 226
227 227 raise Exception(
228 228 'No provider with id={0} found in the config!'.format(short_name))
229 229
230 230
231 231 class ReprMixin(object):
232 232 """
233 233 Provides __repr__() method with output *ClassName(arg1=value, arg2=value)*.
234 234
235 235 Ignored are attributes
236 236
237 237 * which values are considered false.
238 238 * with leading underscore.
239 239 * listed in _repr_ignore.
240 240
241 241 Values of attributes listed in _repr_sensitive will be replaced by *###*.
242 242 Values which repr() string is longer than _repr_length_limit will be
243 243 represented as *ClassName(...)*
244 244
245 245 """
246 246
247 247 #: Iterable of attributes to be ignored.
248 248 _repr_ignore = []
249 249 #: Iterable of attributes which value should not be visible.
250 250 _repr_sensitive = []
251 251 #: `int` Values longer than this will be truncated to *ClassName(...)*.
252 252 _repr_length_limit = 20
253 253
254 254 def __repr__(self):
255 255
256 256 # get class name
257 257 name = self.__class__.__name__
258 258
259 259 # construct keyword arguments
260 260 args = []
261 261
262 262 for k, v in list(self.__dict__.items()):
263 263
264 264 # ignore attributes with leading underscores and those listed in
265 265 # _repr_ignore
266 266 if v and not k.startswith('_') and k not in self._repr_ignore:
267 267
268 268 # replace sensitive values
269 269 if k in self._repr_sensitive:
270 270 v = '###'
271 271
272 272 # if repr is too long
273 273 if len(repr(v)) > self._repr_length_limit:
274 274 # Truncate to ClassName(...)
275 275 v = '{0}(...)'.format(v.__class__.__name__)
276 276 else:
277 277 v = repr(v)
278 278
279 279 args.append('{0}={1}'.format(k, v))
280 280
281 281 return '{0}({1})'.format(name, ', '.join(args))
282 282
283 283
284 284 class Future(threading.Thread):
285 285 """
286 286 Represents an activity run in a separate thread. Subclasses the standard
287 287 library :class:`threading.Thread` and adds :attr:`.get_result` method.
288 288
289 289 .. warning::
290 290
291 291 |async|
292 292
293 293 """
294 294
295 295 def __init__(self, func, *args, **kwargs):
296 296 """
297 297 :param callable func:
298 298 The function to be run in separate thread.
299 299
300 300 Calls :data:`func` in separate thread and returns immediately.
301 301 Accepts arbitrary positional and keyword arguments which will be
302 302 passed to :data:`func`.
303 303 """
304 304
305 305 super(Future, self).__init__()
306 306 self._func = func
307 307 self._args = args
308 308 self._kwargs = kwargs
309 309 self._result = None
310 310
311 311 self.start()
312 312
313 313 def run(self):
314 314 self._result = self._func(*self._args, **self._kwargs)
315 315
316 316 def get_result(self, timeout=None):
317 317 """
318 318 Waits for the wrapped :data:`func` to finish and returns its result.
319 319
320 320 .. note::
321 321
322 322 This will block the **calling thread** until the :data:`func`
323 323 returns.
324 324
325 325 :param timeout:
326 326 :class:`float` or ``None`` A timeout for the :data:`func` to
327 327 return in seconds.
328 328
329 329 :returns:
330 330 The result of the wrapped :data:`func`.
331 331
332 332 """
333 333
334 334 self.join(timeout)
335 335 return self._result
336 336
337 337
338 338 class Session(object):
339 339 """
340 340 A dictionary-like secure cookie session implementation.
341 341 """
342 342
343 343 def __init__(self, adapter, secret, name='authomatic', max_age=600,
344 344 secure=False):
345 345 """
346 346 :param str secret:
347 347 Session secret used to sign the session cookie.
348 348 :param str name:
349 349 Session cookie name.
350 350 :param int max_age:
351 351 Maximum allowed age of session cookie nonce in seconds.
352 352 :param bool secure:
353 353 If ``True`` the session cookie will be saved with ``Secure``
354 354 attribute.
355 355 """
356 356
357 357 self.adapter = adapter
358 358 self.name = name
359 359 self.secret = secret
360 360 self.max_age = max_age
361 361 self.secure = secure
362 362 self._data = {}
363 363
364 364 def create_cookie(self, delete=None):
365 365 """
366 366 Creates the value for ``Set-Cookie`` HTTP header.
367 367
368 368 :param bool delete:
369 369 If ``True`` the cookie value will be ``deleted`` and the
370 370 Expires value will be ``Thu, 01-Jan-1970 00:00:01 GMT``.
371 371
372 372 """
373 373 value = 'deleted' if delete else self._serialize(self.data)
374 374 split_url = parse.urlsplit(self.adapter.url)
375 375 domain = split_url.netloc.split(':')[0]
376 376
377 377 # Work-around for issue #11, failure of WebKit-based browsers to accept
378 378 # cookies set as part of a redirect response in some circumstances.
379 379 if '.' not in domain:
380 380 template = '{name}={value}; Path={path}; HttpOnly{secure}{expires}'
381 381 else:
382 382 template = ('{name}={value}; Domain={domain}; Path={path}; '
383 383 'HttpOnly{secure}{expires}')
384 384
385 385 return template.format(
386 386 name=self.name,
387 387 value=value,
388 388 domain=domain,
389 389 path=split_url.path,
390 390 secure='; Secure' if self.secure else '',
391 391 expires='; Expires=Thu, 01-Jan-1970 00:00:01 GMT' if delete else ''
392 392 )
393 393
394 394 def save(self):
395 395 """
396 396 Adds the session cookie to headers.
397 397 """
398 398 if self.data:
399 399 cookie = self.create_cookie()
400 400 cookie_len = len(cookie)
401 401
402 402 if cookie_len > 4093:
403 403 raise SessionError('Cookie too long! The cookie size {0} '
404 404 'is more than 4093 bytes.'
405 405 .format(cookie_len))
406 406
407 407 self.adapter.set_header('Set-Cookie', cookie)
408 408
409 409 # Reset data
410 410 self._data = {}
411 411
412 412 def delete(self):
413 413 self.adapter.set_header('Set-Cookie', self.create_cookie(delete=True))
414 414
415 415 def _get_data(self):
416 416 """
417 417 Extracts the session data from cookie.
418 418 """
419 419 cookie = self.adapter.cookies.get(self.name)
420 420 return self._deserialize(cookie) if cookie else {}
421 421
422 422 @property
423 423 def data(self):
424 424 """
425 425 Gets session data lazily.
426 426 """
427 427 if not self._data:
428 428 self._data = self._get_data()
429 429 # Always return a dict, even if deserialization returned nothing
430 430 if self._data is None:
431 431 self._data = {}
432 432 return self._data
433 433
434 434 def _signature(self, *parts):
435 435 """
436 436 Creates signature for the session.
437 437 """
438 438 signature = hmac.new(six.b(self.secret), digestmod=hashlib.sha1)
439 439 signature.update(six.b('|'.join(parts)))
440 440 return signature.hexdigest()
441 441
442 442 def _serialize(self, value):
443 443 """
444 444 Converts the value to a signed string with timestamp.
445 445
446 446 :param value:
447 447 Object to be serialized.
448 448
449 449 :returns:
450 450 Serialized value.
451 451
452 452 """
453 453
454 454 # data = copy.deepcopy(value)
455 455 data = value
456 456
457 457 # 1. Serialize
458 458 serialized = pickle.dumps(data).decode('latin-1')
459 459
460 460 # 2. Encode
461 461 # Percent encoding produces smaller result then urlsafe base64.
462 462 encoded = parse.quote(serialized, '')
463 463
464 464 # 3. Concatenate
465 465 timestamp = str(int(time.time()))
466 466 signature = self._signature(self.name, encoded, timestamp)
467 467 concatenated = '|'.join([encoded, timestamp, signature])
468 468
469 469 return concatenated
470 470
471 471 def _deserialize(self, value):
472 472 """
473 473 Deserializes and verifies the value created by :meth:`._serialize`.
474 474
475 475 :param str value:
476 476 The serialized value.
477 477
478 478 :returns:
479 479 Deserialized object.
480 480
481 481 """
482 482
483 483 # 3. Split
484 484 encoded, timestamp, signature = value.split('|')
485 485
486 486 # Verify signature
487 487 if not signature == self._signature(self.name, encoded, timestamp):
488 488 raise SessionError('Invalid signature "{0}"!'.format(signature))
489 489
490 490 # Verify timestamp
491 491 if int(timestamp) < int(time.time()) - self.max_age:
492 492 return None
493 493
494 494 # 2. Decode
495 495 decoded = parse.unquote(encoded)
496 496
497 497 # 1. Deserialize
498 498 deserialized = pickle.loads(decoded.encode('latin-1'))
499 499
500 500 return deserialized
501 501
502 502 def __setitem__(self, key, value):
503 503 self._data[key] = value
504 504
505 505 def __getitem__(self, key):
506 506 return self.data.__getitem__(key)
507 507
508 508 def __delitem__(self, key):
509 509 return self._data.__delitem__(key)
510 510
511 511 def get(self, key, default=None):
512 512 return self.data.get(key, default)
513 513
514 514
515 515 class User(ReprMixin):
516 516 """
517 517 Provides unified interface to selected **user** info returned by different
518 518 **providers**.
519 519
520 520 .. note:: The value format may vary across providers.
521 521
522 522 """
523 523
524 524 def __init__(self, provider, **kwargs):
525 525 #: A :doc:`provider <providers>` instance.
526 526 self.provider = provider
527 527
528 528 #: An :class:`.Credentials` instance.
529 529 self.credentials = kwargs.get('credentials')
530 530
531 531 #: A :class:`dict` containing all the **user** information returned
532 532 #: by the **provider**.
533 533 #: The structure differs across **providers**.
534 534 self.data = kwargs.get('data')
535 535
536 536 #: The :attr:`.Response.content` of the request made to update
537 537 #: the user.
538 538 self.content = kwargs.get('content')
539 539
540 540 #: :class:`str` ID assigned to the **user** by the **provider**.
541 541 self.id = kwargs.get('id')
542 542 #: :class:`str` User name e.g. *andrewpipkin*.
543 543 self.username = kwargs.get('username')
544 544 #: :class:`str` Name e.g. *Andrew Pipkin*.
545 545 self.name = kwargs.get('name')
546 546 #: :class:`str` First name e.g. *Andrew*.
547 547 self.first_name = kwargs.get('first_name')
548 548 #: :class:`str` Last name e.g. *Pipkin*.
549 549 self.last_name = kwargs.get('last_name')
550 550 #: :class:`str` Nickname e.g. *Andy*.
551 551 self.nickname = kwargs.get('nickname')
552 552 #: :class:`str` Link URL.
553 553 self.link = kwargs.get('link')
554 554 #: :class:`str` Gender.
555 555 self.gender = kwargs.get('gender')
556 556 #: :class:`str` Timezone.
557 557 self.timezone = kwargs.get('timezone')
558 558 #: :class:`str` Locale.
559 559 self.locale = kwargs.get('locale')
560 560 #: :class:`str` E-mail.
561 561 self.email = kwargs.get('email')
562 562 #: :class:`str` phone.
563 563 self.phone = kwargs.get('phone')
564 564 #: :class:`str` Picture URL.
565 565 self.picture = kwargs.get('picture')
566 566 #: Birth date as :class:`datetime.datetime()` or :class:`str`
567 567 # if parsing failed or ``None``.
568 568 self.birth_date = kwargs.get('birth_date')
569 569 #: :class:`str` Country.
570 570 self.country = kwargs.get('country')
571 571 #: :class:`str` City.
572 572 self.city = kwargs.get('city')
573 573 #: :class:`str` Geographical location.
574 574 self.location = kwargs.get('location')
575 575 #: :class:`str` Postal code.
576 576 self.postal_code = kwargs.get('postal_code')
577 577 #: Instance of the Google App Engine Users API
578 578 #: `User <https://developers.google.com/appengine/docs/python/users/userclass>`_ class.
579 579 #: Only present when using the :class:`authomatic.providers.gaeopenid.GAEOpenID` provider.
580 580 self.gae_user = kwargs.get('gae_user')
581 581
582 582 def update(self):
583 583 """
584 584 Updates the user info by fetching the **provider's** user info URL.
585 585
586 586 :returns:
587 587 Updated instance of this class.
588 588
589 589 """
590 590
591 591 return self.provider.update_user()
592 592
593 593 def async_update(self):
594 594 """
595 595 Same as :meth:`.update` but runs asynchronously in a separate thread.
596 596
597 597 .. warning::
598 598
599 599 |async|
600 600
601 601 :returns:
602 602 :class:`.Future` instance representing the separate thread.
603 603
604 604 """
605 605
606 606 return Future(self.update)
607 607
608 608 def to_dict(self):
609 609 """
610 610 Converts the :class:`.User` instance to a :class:`dict`.
611 611
612 612 :returns:
613 613 :class:`dict`
614 614
615 615 """
616 616
617 617 # copy the dictionary
618 618 d = copy.copy(self.__dict__)
619 619
620 620 # Keep only the provider name to avoid circular reference
621 621 d['provider'] = self.provider.name
622 622 d['credentials'] = self.credentials.serialize(
623 623 ) if self.credentials else None
624 624 d['birth_date'] = str(d['birth_date'])
625 625
626 626 # Remove content
627 627 d.pop('content')
628 628
629 629 if isinstance(self.data, ElementTree.Element):
630 630 d['data'] = None
631 631
632 632 return d
633 633
634 634
635 635 SupportedUserAttributesNT = collections.namedtuple(
636 636 typename='SupportedUserAttributesNT',
637 637 field_names=['birth_date', 'city', 'country', 'email', 'first_name',
638 638 'gender', 'id', 'last_name', 'link', 'locale', 'location',
639 639 'name', 'nickname', 'phone', 'picture', 'postal_code',
640 640 'timezone', 'username', ]
641 641 )
642 642
643 643
644 644 class SupportedUserAttributes(SupportedUserAttributesNT):
645 645 def __new__(cls, **kwargs):
646 646 defaults = dict((i, False) for i in SupportedUserAttributes._fields) # pylint:disable=no-member
647 647 defaults.update(**kwargs)
648 648 return super(SupportedUserAttributes, cls).__new__(cls, **defaults)
649 649
650 650
651 651 class Credentials(ReprMixin):
652 652 """
653 653 Contains all necessary information to fetch **user's protected resources**.
654 654 """
655 655
656 656 _repr_sensitive = ('token', 'refresh_token', 'token_secret',
657 657 'consumer_key', 'consumer_secret')
658 658
659 659 def __init__(self, config, **kwargs):
660 660
661 661 #: :class:`dict` :doc:`config`.
662 662 self.config = config
663 663
664 664 #: :class:`str` User **access token**.
665 665 self.token = kwargs.get('token', '')
666 666
667 667 #: :class:`str` Access token type.
668 668 self.token_type = kwargs.get('token_type', '')
669 669
670 670 #: :class:`str` Refresh token.
671 671 self.refresh_token = kwargs.get('refresh_token', '')
672 672
673 673 #: :class:`str` Access token secret.
674 674 self.token_secret = kwargs.get('token_secret', '')
675 675
676 676 #: :class:`int` Expiration date as UNIX timestamp.
677 677 self.expiration_time = int(kwargs.get('expiration_time', 0))
678 678
679 679 #: A :doc:`Provider <providers>` instance**.
680 680 provider = kwargs.get('provider')
681 681
682 682 self.expire_in = int(kwargs.get('expire_in', 0))
683 683
684 684 if provider:
685 685 #: :class:`str` Provider name specified in the :doc:`config`.
686 686 self.provider_name = provider.name
687 687
688 688 #: :class:`str` Provider type e.g.
689 689 # ``"authomatic.providers.oauth2.OAuth2"``.
690 690 self.provider_type = provider.get_type()
691 691
692 692 #: :class:`str` Provider type e.g.
693 693 # ``"authomatic.providers.oauth2.OAuth2"``.
694 694 self.provider_type_id = provider.type_id
695 695
696 696 #: :class:`str` Provider short name specified in the :doc:`config`.
697 697 self.provider_id = int(provider.id) if provider.id else None
698 698
699 699 #: :class:`class` Provider class.
700 700 self.provider_class = provider.__class__
701 701
702 702 #: :class:`str` Consumer key specified in the :doc:`config`.
703 703 self.consumer_key = provider.consumer_key
704 704
705 705 #: :class:`str` Consumer secret specified in the :doc:`config`.
706 706 self.consumer_secret = provider.consumer_secret
707 707
708 708 else:
709 709 self.provider_name = kwargs.get('provider_name', '')
710 710 self.provider_type = kwargs.get('provider_type', '')
711 711 self.provider_type_id = kwargs.get('provider_type_id')
712 712 self.provider_id = kwargs.get('provider_id')
713 713 self.provider_class = kwargs.get('provider_class')
714 714
715 715 self.consumer_key = kwargs.get('consumer_key', '')
716 716 self.consumer_secret = kwargs.get('consumer_secret', '')
717 717
718 718 @property
719 719 def expire_in(self):
720 720 """
721 721
722 722 """
723 723
724 724 return self._expire_in
725 725
726 726 @expire_in.setter
727 727 def expire_in(self, value):
728 728 """
729 729 Computes :attr:`.expiration_time` when the value is set.
730 730 """
731 731
732 732 # pylint:disable=attribute-defined-outside-init
733 733 if value:
734 734 self._expiration_time = int(time.time()) + int(value)
735 735 self._expire_in = value
736 736
737 737 @property
738 738 def expiration_time(self):
739 739 return self._expiration_time
740 740
741 741 @expiration_time.setter
742 742 def expiration_time(self, value):
743 743
744 744 # pylint:disable=attribute-defined-outside-init
745 745 self._expiration_time = int(value)
746 746 self._expire_in = self._expiration_time - int(time.time())
747 747
748 748 @property
749 749 def expiration_date(self):
750 750 """
751 751 Expiration date as :class:`datetime.datetime` or ``None`` if
752 752 credentials never expire.
753 753 """
754 754
755 755 if self.expire_in < 0:
756 756 return None
757 757 else:
758 758 return datetime.datetime.fromtimestamp(self.expiration_time)
759 759
760 760 @property
761 761 def valid(self):
762 762 """
763 763 ``True`` if credentials are valid, ``False`` if expired.
764 764 """
765 765
766 766 if self.expiration_time:
767 767 return self.expiration_time > int(time.time())
768 768 else:
769 769 return True
770 770
771 771 def expire_soon(self, seconds):
772 772 """
773 773 Returns ``True`` if credentials expire sooner than specified.
774 774
775 775 :param int seconds:
776 776 Number of seconds.
777 777
778 778 :returns:
779 779 ``True`` if credentials expire sooner than specified,
780 780 else ``False``.
781 781
782 782 """
783 783
784 784 if self.expiration_time:
785 785 return self.expiration_time < int(time.time()) + int(seconds)
786 786 else:
787 787 return False
788 788
789 789 def refresh(self, force=False, soon=86400):
790 790 """
791 791 Refreshes the credentials only if the **provider** supports it and if
792 792 it will expire in less than one day. It does nothing in other cases.
793 793
794 794 .. note::
795 795
796 796 The credentials will be refreshed only if it gives sense
797 797 i.e. only |oauth2|_ has the notion of credentials
798 798 *refreshment/extension*.
799 799 And there are also differences across providers e.g. Google
800 800 supports refreshment only if there is a ``refresh_token`` in
801 801 the credentials and that in turn is present only if the
802 802 ``access_type`` parameter was set to ``offline`` in the
803 803 **user authorization request**.
804 804
805 805 :param bool force:
806 806 If ``True`` the credentials will be refreshed even if they
807 807 won't expire soon.
808 808
809 809 :param int soon:
810 810 Number of seconds specifying what means *soon*.
811 811
812 812 """
813 813
814 814 if hasattr(self.provider_class, 'refresh_credentials'):
815 815 if force or self.expire_soon(soon):
816 816 logging.info('PROVIDER NAME: {0}'.format(self.provider_name))
817 817 return self.provider_class(
818 818 self, None, self.provider_name).refresh_credentials(self)
819 819
820 820 def async_refresh(self, *args, **kwargs):
821 821 """
822 822 Same as :meth:`.refresh` but runs asynchronously in a separate thread.
823 823
824 824 .. warning::
825 825
826 826 |async|
827 827
828 828 :returns:
829 829 :class:`.Future` instance representing the separate thread.
830 830
831 831 """
832 832
833 833 return Future(self.refresh, *args, **kwargs)
834 834
835 835 def provider_type_class(self):
836 836 """
837 837 Returns the :doc:`provider <providers>` class specified in the
838 838 :doc:`config`.
839 839
840 840 :returns:
841 841 :class:`authomatic.providers.BaseProvider` subclass.
842 842
843 843 """
844 844
845 845 return resolve_provider_class(self.provider_type)
846 846
847 847 def serialize(self):
848 848 """
849 849 Converts the credentials to a percent encoded string to be stored for
850 850 later use.
851 851
852 852 :returns:
853 853 :class:`string`
854 854
855 855 """
856 856
857 857 if self.provider_id is None:
858 858 raise ConfigError(
859 859 'To serialize credentials you need to specify a '
860 860 'unique integer under the "id" key in the config '
861 861 'for each provider!')
862 862
863 863 # Get the provider type specific items.
864 864 rest = self.provider_type_class().to_tuple(self)
865 865
866 866 # Provider ID and provider type ID are always the first two items.
867 867 result = (self.provider_id, self.provider_type_id) + rest
868 868
869 869 # Make sure that all items are strings.
870 870 stringified = [str(i) for i in result]
871 871
872 872 # Concatenate by newline.
873 873 concatenated = '\n'.join(stringified)
874 874
875 875 # Percent encode.
876 876 return parse.quote(concatenated, '')
877 877
878 878 @classmethod
879 879 def deserialize(cls, config, credentials):
880 880 """
881 881 A *class method* which reconstructs credentials created by
882 882 :meth:`serialize`. You can also pass it a :class:`.Credentials`
883 883 instance.
884 884
885 885 :param dict config:
886 886 The same :doc:`config` used in the :func:`.login` to get the
887 887 credentials.
888 888 :param str credentials:
889 889 :class:`string` The serialized credentials or
890 890 :class:`.Credentials` instance.
891 891
892 892 :returns:
893 893 :class:`.Credentials`
894 894
895 895 """
896 896
897 897 # Accept both serialized and normal.
898 898 if isinstance(credentials, Credentials):
899 899 return credentials
900 900
901 901 decoded = parse.unquote(credentials)
902 902
903 903 split = decoded.split('\n')
904 904
905 905 # We need the provider ID to move forward.
906 906 if split[0] is None:
907 907 raise CredentialsError(
908 908 'To deserialize credentials you need to specify a unique '
909 909 'integer under the "id" key in the config for each provider!')
910 910
911 911 # Get provider config by short name.
912 912 provider_name = id_to_name(config, int(split[0]))
913 913 cfg = config.get(provider_name)
914 914
915 915 # Get the provider class.
916 916 ProviderClass = resolve_provider_class(cfg.get('class_'))
917 917
918 918 deserialized = Credentials(config)
919 919
920 920 deserialized.provider_id = int(split[0])
921 921 deserialized.provider_type = ProviderClass.get_type()
922 922 deserialized.provider_type_id = split[1]
923 923 deserialized.provider_class = ProviderClass
924 924 deserialized.provider_name = provider_name
925 925 deserialized.provider_class = ProviderClass
926 926
927 927 # Add provider type specific properties.
928 928 return ProviderClass.reconstruct(split[2:], deserialized, cfg)
929 929
930 930
931 931 class LoginResult(ReprMixin):
932 932 """
933 933 Result of the :func:`authomatic.login` function.
934 934 """
935 935
936 936 def __init__(self, provider):
937 937 #: A :doc:`provider <providers>` instance.
938 938 self.provider = provider
939 939
940 940 #: An instance of the :exc:`authomatic.exceptions.BaseError` subclass.
941 941 self.error = None
942 942
943 943 def popup_js(self, callback_name=None, indent=None,
944 944 custom=None, stay_open=False):
945 945 """
946 946 Returns JavaScript that:
947 947
948 948 #. Triggers the ``options.onLoginComplete(result, closer)``
949 949 handler set with the :ref:`authomatic.setup() <js_setup>`
950 950 function of :ref:`javascript.js <js>`.
951 951 #. Calls the JavasScript callback specified by :data:`callback_name`
952 952 on the opener of the *login handler popup* and passes it the
953 953 *login result* JSON object as first argument and the `closer`
954 954 function which you should call in your callback to close the popup.
955 955
956 956 :param str callback_name:
957 957 The name of the javascript callback e.g ``foo.bar.loginCallback``
958 958 will result in ``window.opener.foo.bar.loginCallback(result);``
959 959 in the HTML.
960 960
961 961 :param int indent:
962 962 The number of spaces to indent the JSON result object.
963 963 If ``0`` or negative, only newlines are added.
964 964 If ``None``, no newlines are added.
965 965
966 966 :param custom:
967 967 Any JSON serializable object that will be passed to the
968 968 ``result.custom`` attribute.
969 969
970 970 :param str stay_open:
971 971 If ``True``, the popup will stay open.
972 972
973 973 :returns:
974 974 :class:`str` with JavaScript.
975 975
976 976 """
977 977
978 978 custom_callback = """
979 979 try {{ window.opener.{cb}(result, closer); }} catch(e) {{}}
980 980 """.format(cb=callback_name) if callback_name else ''
981 981
982 982 # TODO: Move the window.close() to the opener
983 983 return """
984 984 (function(){{
985 985
986 986 closer = function(){{
987 987 window.close();
988 988 }};
989 989
990 990 var result = {result};
991 991 result.custom = {custom};
992 992
993 993 {custom_callback}
994 994
995 995 try {{
996 996 window.opener.authomatic.loginComplete(result, closer);
997 997 }} catch(e) {{}}
998 998
999 999 }})();
1000 1000
1001 1001 """.format(result=self.to_json(indent),
1002 1002 custom=json.dumps(custom),
1003 1003 custom_callback=custom_callback,
1004 1004 stay_open='// ' if stay_open else '')
1005 1005
1006 1006 def popup_html(self, callback_name=None, indent=None,
1007 1007 title='Login | {0}', custom=None, stay_open=False):
1008 1008 """
1009 1009 Returns a HTML with JavaScript that:
1010 1010
1011 1011 #. Triggers the ``options.onLoginComplete(result, closer)`` handler
1012 1012 set with the :ref:`authomatic.setup() <js_setup>` function of
1013 1013 :ref:`javascript.js <js>`.
1014 1014 #. Calls the JavasScript callback specified by :data:`callback_name`
1015 1015 on the opener of the *login handler popup* and passes it the
1016 1016 *login result* JSON object as first argument and the `closer`
1017 1017 function which you should call in your callback to close the popup.
1018 1018
1019 1019 :param str callback_name:
1020 1020 The name of the javascript callback e.g ``foo.bar.loginCallback``
1021 1021 will result in ``window.opener.foo.bar.loginCallback(result);``
1022 1022 in the HTML.
1023 1023
1024 1024 :param int indent:
1025 1025 The number of spaces to indent the JSON result object.
1026 1026 If ``0`` or negative, only newlines are added.
1027 1027 If ``None``, no newlines are added.
1028 1028
1029 1029 :param str title:
1030 1030 The text of the HTML title. You can use ``{0}`` tag inside,
1031 1031 which will be replaced by the provider name.
1032 1032
1033 1033 :param custom:
1034 1034 Any JSON serializable object that will be passed to the
1035 1035 ``result.custom`` attribute.
1036 1036
1037 1037 :param str stay_open:
1038 1038 If ``True``, the popup will stay open.
1039 1039
1040 1040 :returns:
1041 1041 :class:`str` with HTML.
1042 1042
1043 1043 """
1044 1044
1045 1045 return """
1046 1046 <!DOCTYPE html>
1047 1047 <html>
1048 1048 <head><title>{title}</title></head>
1049 1049 <body>
1050 1050 <script type="text/javascript">
1051 1051 {js}
1052 1052 </script>
1053 1053 </body>
1054 1054 </html>
1055 1055 """.format(
1056 1056 title=title.format(self.provider.name if self.provider else ''),
1057 1057 js=self.popup_js(callback_name, indent, custom, stay_open)
1058 1058 )
1059 1059
1060 1060 @property
1061 1061 def user(self):
1062 1062 """
1063 1063 A :class:`.User` instance.
1064 1064 """
1065 1065
1066 1066 return self.provider.user if self.provider else None
1067 1067
1068 1068 def to_dict(self):
1069 1069 return dict(provider=self.provider, user=self.user, error=self.error)
1070 1070
1071 1071 def to_json(self, indent=4):
1072 1072 return json.dumps(self, default=lambda obj: obj.to_dict(
1073 1073 ) if hasattr(obj, 'to_dict') else '', indent=indent)
1074 1074
1075 1075
1076 1076 class Response(ReprMixin):
1077 1077 """
1078 1078 Wraps :class:`httplib.HTTPResponse` and adds.
1079 1079
1080 1080 :attr:`.content` and :attr:`.data` attributes.
1081 1081
1082 1082 """
1083 1083
1084 1084 def __init__(self, httplib_response, content_parser=None):
1085 1085 """
1086 1086 :param httplib_response:
1087 1087 The wrapped :class:`httplib.HTTPResponse` instance.
1088 1088
1089 1089 :param function content_parser:
1090 1090 Callable which accepts :attr:`.content` as argument,
1091 1091 parses it and returns the parsed data as :class:`dict`.
1092 1092 """
1093 1093
1094 1094 self.httplib_response = httplib_response
1095 1095 self.content_parser = content_parser or json_qs_parser
1096 1096 self._data = None
1097 1097 self._content = None
1098 1098
1099 1099 #: Same as :attr:`httplib.HTTPResponse.msg`.
1100 1100 self.msg = httplib_response.msg
1101 1101 #: Same as :attr:`httplib.HTTPResponse.version`.
1102 1102 self.version = httplib_response.version
1103 1103 #: Same as :attr:`httplib.HTTPResponse.status`.
1104 1104 self.status = httplib_response.status
1105 1105 #: Same as :attr:`httplib.HTTPResponse.reason`.
1106 1106 self.reason = httplib_response.reason
1107 1107
1108 1108 def read(self, amt=None):
1109 1109 """
1110 1110 Same as :meth:`httplib.HTTPResponse.read`.
1111 1111
1112 1112 :param amt:
1113 1113
1114 1114 """
1115 1115
1116 1116 return self.httplib_response.read(amt)
1117 1117
1118 1118 def getheader(self, name, default=None):
1119 1119 """
1120 1120 Same as :meth:`httplib.HTTPResponse.getheader`.
1121 1121
1122 1122 :param name:
1123 1123 :param default:
1124 1124
1125 1125 """
1126 1126
1127 1127 return self.httplib_response.getheader(name, default)
1128 1128
1129 1129 def fileno(self):
1130 1130 """
1131 1131 Same as :meth:`httplib.HTTPResponse.fileno`.
1132 1132 """
1133 1133 return self.httplib_response.fileno()
1134 1134
1135 1135 def getheaders(self):
1136 1136 """
1137 1137 Same as :meth:`httplib.HTTPResponse.getheaders`.
1138 1138 """
1139 1139 return self.httplib_response.getheaders()
1140 1140
1141 1141 @staticmethod
1142 1142 def is_binary_string(content):
1143 1143 """
1144 1144 Return true if string is binary data.
1145 1145 """
1146 1146
1147 1147 textchars = (bytearray([7, 8, 9, 10, 12, 13, 27])
1148 1148 + bytearray(range(0x20, 0x100)))
1149 1149 return bool(content.translate(None, textchars))
1150 1150
1151 1151 @property
1152 1152 def content(self):
1153 1153 """
1154 1154 The whole response content.
1155 1155 """
1156 1156
1157 1157 if not self._content:
1158 1158 content = self.httplib_response.read()
1159 1159 if self.is_binary_string(content):
1160 1160 self._content = content
1161 1161 else:
1162 #self._content = content.decode('utf-8')
1163 self._content = content
1162 self._content = content.decode('utf-8')
1164 1163 return self._content
1165 1164
1166 1165 @property
1167 1166 def data(self):
1168 1167 """
1169 1168 A :class:`dict` of data parsed from :attr:`.content`.
1170 1169 """
1171 1170
1172 1171 if not self._data:
1173 1172 self._data = self.content_parser(self.content)
1174 1173 return self._data
1175 1174
1176 1175
1177 1176 class UserInfoResponse(Response):
1178 1177 """
1179 1178 Inherits from :class:`.Response`, adds :attr:`~UserInfoResponse.user`
1180 1179 attribute.
1181 1180 """
1182 1181
1183 1182 def __init__(self, user, *args, **kwargs):
1184 1183 super(UserInfoResponse, self).__init__(*args, **kwargs)
1185 1184
1186 1185 #: :class:`.User` instance.
1187 1186 self.user = user
1188 1187
1189 1188
1190 1189 class RequestElements(tuple):
1191 1190 """
1192 1191 A tuple of ``(url, method, params, headers, body)`` request elements.
1193 1192
1194 1193 With some additional properties.
1195 1194
1196 1195 """
1197 1196
1198 1197 def __new__(cls, url, method, params, headers, body):
1199 1198 return tuple.__new__(cls, (url, method, params, headers, body))
1200 1199
1201 1200 @property
1202 1201 def url(self):
1203 1202 """
1204 1203 Request URL.
1205 1204 """
1206 1205
1207 1206 return self[0]
1208 1207
1209 1208 @property
1210 1209 def method(self):
1211 1210 """
1212 1211 HTTP method of the request.
1213 1212 """
1214 1213
1215 1214 return self[1]
1216 1215
1217 1216 @property
1218 1217 def params(self):
1219 1218 """
1220 1219 Dictionary of request parameters.
1221 1220 """
1222 1221
1223 1222 return self[2]
1224 1223
1225 1224 @property
1226 1225 def headers(self):
1227 1226 """
1228 1227 Dictionary of request headers.
1229 1228 """
1230 1229
1231 1230 return self[3]
1232 1231
1233 1232 @property
1234 1233 def body(self):
1235 1234 """
1236 1235 :class:`str` Body of ``POST``, ``PUT`` and ``PATCH`` requests.
1237 1236 """
1238 1237
1239 1238 return self[4]
1240 1239
1241 1240 @property
1242 1241 def query_string(self):
1243 1242 """
1244 1243 Query string of the request.
1245 1244 """
1246 1245
1247 1246 return parse.urlencode(self.params)
1248 1247
1249 1248 @property
1250 1249 def full_url(self):
1251 1250 """
1252 1251 URL with query string.
1253 1252 """
1254 1253
1255 1254 return self.url + '?' + self.query_string
1256 1255
1257 1256 def to_json(self):
1258 1257 return json.dumps(dict(url=self.url,
1259 1258 method=self.method,
1260 1259 params=self.params,
1261 1260 headers=self.headers,
1262 1261 body=self.body))
1263 1262
1264 1263
1265 1264 class Authomatic(object):
1266 1265 def __init__(
1267 1266 self, config, secret, session_max_age=600, secure_cookie=False,
1268 1267 session=None, session_save_method=None, report_errors=True,
1269 1268 debug=False, logging_level=logging.INFO, prefix='authomatic',
1270 1269 logger=None
1271 1270 ):
1272 1271 """
1273 1272 Encapsulates all the functionality of this package.
1274 1273
1275 1274 :param dict config:
1276 1275 :doc:`config`
1277 1276
1278 1277 :param str secret:
1279 1278 A secret string that will be used as the key for signing
1280 1279 :class:`.Session` cookie and as a salt by *CSRF* token generation.
1281 1280
1282 1281 :param session_max_age:
1283 1282 Maximum allowed age of :class:`.Session` cookie nonce in seconds.
1284 1283
1285 1284 :param bool secure_cookie:
1286 1285 If ``True`` the :class:`.Session` cookie will be saved wit
1287 1286 ``Secure`` attribute.
1288 1287
1289 1288 :param session:
1290 1289 Custom dictionary-like session implementation.
1291 1290
1292 1291 :param callable session_save_method:
1293 1292 A method of the supplied session or any mechanism that saves the
1294 1293 session data and cookie.
1295 1294
1296 1295 :param bool report_errors:
1297 1296 If ``True`` exceptions encountered during the **login procedure**
1298 1297 will be caught and reported in the :attr:`.LoginResult.error`
1299 1298 attribute.
1300 1299 Default is ``True``.
1301 1300
1302 1301 :param bool debug:
1303 1302 If ``True`` traceback of exceptions will be written to response.
1304 1303 Default is ``False``.
1305 1304
1306 1305 :param int logging_level:
1307 1306 The logging level threshold for the default logger as specified in
1308 1307 the standard Python
1309 1308 `logging library <http://docs.python.org/2/library/logging.html>`_.
1310 1309 This setting is ignored when :data:`logger` is set.
1311 1310 Default is ``logging.INFO``.
1312 1311
1313 1312 :param str prefix:
1314 1313 Prefix used as the :class:`.Session` cookie name.
1315 1314
1316 1315 :param logger:
1317 1316 A :class:`logging.logger` instance.
1318 1317
1319 1318 """
1320 1319
1321 1320 self.config = config
1322 1321 self.secret = secret
1323 1322 self.session_max_age = session_max_age
1324 1323 self.secure_cookie = secure_cookie
1325 1324 self.session = session
1326 1325 self.session_save_method = session_save_method
1327 1326 self.report_errors = report_errors
1328 1327 self.debug = debug
1329 1328 self.logging_level = logging_level
1330 1329 self.prefix = prefix
1331 1330 self._logger = logger or logging.getLogger(str(id(self)))
1332 1331
1333 1332 # Set logging level.
1334 1333 if logger is None:
1335 1334 self._logger.setLevel(logging_level)
1336 1335
1337 1336 def login(self, adapter, provider_name, callback=None,
1338 1337 session=None, session_saver=None, **kwargs):
1339 1338 """
1340 1339 If :data:`provider_name` specified, launches the login procedure for
1341 1340 corresponding :doc:`provider </reference/providers>` and returns
1342 1341 :class:`.LoginResult`.
1343 1342
1344 1343 If :data:`provider_name` is empty, acts like
1345 1344 :meth:`.Authomatic.backend`.
1346 1345
1347 1346 .. warning::
1348 1347
1349 1348 The method redirects the **user** to the **provider** which in
1350 1349 turn redirects **him/her** back to the *request handler* where
1351 1350 it has been called.
1352 1351
1353 1352 :param str provider_name:
1354 1353 Name of the provider as specified in the keys of the :doc:`config`.
1355 1354
1356 1355 :param callable callback:
1357 1356 If specified the method will call the callback with
1358 1357 :class:`.LoginResult` passed as argument and will return nothing.
1359 1358
1360 1359 :param bool report_errors:
1361 1360
1362 1361 .. note::
1363 1362
1364 1363 Accepts additional keyword arguments that will be passed to
1365 1364 :doc:`provider <providers>` constructor.
1366 1365
1367 1366 :returns:
1368 1367 :class:`.LoginResult`
1369 1368
1370 1369 """
1371 1370
1372 1371 if provider_name:
1373 1372 # retrieve required settings for current provider and raise
1374 1373 # exceptions if missing
1375 1374 provider_settings = self.config.get(provider_name)
1376 1375 if not provider_settings:
1377 1376 raise ConfigError('Provider name "{0}" not specified!'
1378 1377 .format(provider_name))
1379 1378
1380 1379 if not (session is None or session_saver is None):
1381 1380 session = session
1382 1381 session_saver = session_saver
1383 1382 else:
1384 1383 session = Session(adapter=adapter,
1385 1384 secret=self.secret,
1386 1385 max_age=self.session_max_age,
1387 1386 name=self.prefix,
1388 1387 secure=self.secure_cookie)
1389 1388
1390 1389 session_saver = session.save
1391 1390
1392 1391 # Resolve provider class.
1393 1392 class_ = provider_settings.get('class_')
1394 1393 if not class_:
1395 1394 raise ConfigError(
1396 1395 'The "class_" key not specified in the config'
1397 1396 ' for provider {0}!'.format(provider_name))
1398 1397 ProviderClass = resolve_provider_class(class_)
1399 1398
1400 1399 # FIXME: Find a nicer solution
1401 1400 ProviderClass._logger = self._logger
1402 1401
1403 1402 # instantiate provider class
1404 1403 provider = ProviderClass(self,
1405 1404 adapter=adapter,
1406 1405 provider_name=provider_name,
1407 1406 callback=callback,
1408 1407 session=session,
1409 1408 session_saver=session_saver,
1410 1409 **kwargs)
1411 1410
1412 1411 # return login result
1413 1412 return provider.login()
1414 1413
1415 1414 else:
1416 1415 # Act like backend.
1417 1416 self.backend(adapter)
1418 1417
1419 1418 def credentials(self, credentials):
1420 1419 """
1421 1420 Deserializes credentials.
1422 1421
1423 1422 :param credentials:
1424 1423 Credentials serialized with :meth:`.Credentials.serialize` or
1425 1424 :class:`.Credentials` instance.
1426 1425
1427 1426 :returns:
1428 1427 :class:`.Credentials`
1429 1428
1430 1429 """
1431 1430
1432 1431 return Credentials.deserialize(self.config, credentials)
1433 1432
1434 1433 def access(self, credentials, url, params=None, method='GET',
1435 1434 headers=None, body='', max_redirects=5, content_parser=None):
1436 1435 """
1437 1436 Accesses **protected resource** on behalf of the **user**.
1438 1437
1439 1438 :param credentials:
1440 1439 The **user's** :class:`.Credentials` (serialized or normal).
1441 1440
1442 1441 :param str url:
1443 1442 The **protected resource** URL.
1444 1443
1445 1444 :param str method:
1446 1445 HTTP method of the request.
1447 1446
1448 1447 :param dict headers:
1449 1448 HTTP headers of the request.
1450 1449
1451 1450 :param str body:
1452 1451 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
1453 1452
1454 1453 :param int max_redirects:
1455 1454 Maximum number of HTTP redirects to follow.
1456 1455
1457 1456 :param function content_parser:
1458 1457 A function to be used to parse the :attr:`.Response.data`
1459 1458 from :attr:`.Response.content`.
1460 1459
1461 1460 :returns:
1462 1461 :class:`.Response`
1463 1462
1464 1463 """
1465 1464
1466 1465 # Deserialize credentials.
1467 1466 credentials = Credentials.deserialize(self.config, credentials)
1468 1467
1469 1468 # Resolve provider class.
1470 1469 ProviderClass = credentials.provider_class
1471 1470 logging.info('ACCESS HEADERS: {0}'.format(headers))
1472 1471 # Access resource and return response.
1473 1472
1474 1473 provider = ProviderClass(
1475 1474 self, adapter=None, provider_name=credentials.provider_name)
1476 1475 provider.credentials = credentials
1477 1476
1478 1477 return provider.access(url=url,
1479 1478 params=params,
1480 1479 method=method,
1481 1480 headers=headers,
1482 1481 body=body,
1483 1482 max_redirects=max_redirects,
1484 1483 content_parser=content_parser)
1485 1484
1486 1485 def async_access(self, *args, **kwargs):
1487 1486 """
1488 1487 Same as :meth:`.Authomatic.access` but runs asynchronously in a
1489 1488 separate thread.
1490 1489
1491 1490 .. warning::
1492 1491
1493 1492 |async|
1494 1493
1495 1494 :returns:
1496 1495 :class:`.Future` instance representing the separate thread.
1497 1496
1498 1497 """
1499 1498
1500 1499 return Future(self.access, *args, **kwargs)
1501 1500
1502 1501 def request_elements(
1503 1502 self, credentials=None, url=None, method='GET', params=None,
1504 1503 headers=None, body='', json_input=None, return_json=False
1505 1504 ):
1506 1505 """
1507 1506 Creates request elements for accessing **protected resource of a
1508 1507 user**. Required arguments are :data:`credentials` and :data:`url`. You
1509 1508 can pass :data:`credentials`, :data:`url`, :data:`method`, and
1510 1509 :data:`params` as a JSON object.
1511 1510
1512 1511 :param credentials:
1513 1512 The **user's** credentials (can be serialized).
1514 1513
1515 1514 :param str url:
1516 1515 The url of the protected resource.
1517 1516
1518 1517 :param str method:
1519 1518 The HTTP method of the request.
1520 1519
1521 1520 :param dict params:
1522 1521 Dictionary of request parameters.
1523 1522
1524 1523 :param dict headers:
1525 1524 Dictionary of request headers.
1526 1525
1527 1526 :param str body:
1528 1527 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
1529 1528
1530 1529 :param str json_input:
1531 1530 you can pass :data:`credentials`, :data:`url`, :data:`method`,
1532 1531 :data:`params` and :data:`headers` in a JSON object.
1533 1532 Values from arguments will be used for missing properties.
1534 1533
1535 1534 ::
1536 1535
1537 1536 {
1538 1537 "credentials": "###",
1539 1538 "url": "https://example.com/api",
1540 1539 "method": "POST",
1541 1540 "params": {
1542 1541 "foo": "bar"
1543 1542 },
1544 1543 "headers": {
1545 1544 "baz": "bing",
1546 1545 "Authorization": "Bearer ###"
1547 1546 },
1548 1547 "body": "Foo bar baz bing."
1549 1548 }
1550 1549
1551 1550 :param bool return_json:
1552 1551 if ``True`` the function returns a json object.
1553 1552
1554 1553 ::
1555 1554
1556 1555 {
1557 1556 "url": "https://example.com/api",
1558 1557 "method": "POST",
1559 1558 "params": {
1560 1559 "access_token": "###",
1561 1560 "foo": "bar"
1562 1561 },
1563 1562 "headers": {
1564 1563 "baz": "bing",
1565 1564 "Authorization": "Bearer ###"
1566 1565 },
1567 1566 "body": "Foo bar baz bing."
1568 1567 }
1569 1568
1570 1569 :returns:
1571 1570 :class:`.RequestElements` or JSON string.
1572 1571
1573 1572 """
1574 1573
1575 1574 # Parse values from JSON
1576 1575 if json_input:
1577 1576 parsed_input = json.loads(json_input)
1578 1577
1579 1578 credentials = parsed_input.get('credentials', credentials)
1580 1579 url = parsed_input.get('url', url)
1581 1580 method = parsed_input.get('method', method)
1582 1581 params = parsed_input.get('params', params)
1583 1582 headers = parsed_input.get('headers', headers)
1584 1583 body = parsed_input.get('body', body)
1585 1584
1586 1585 if not credentials and url:
1587 1586 raise RequestElementsError(
1588 1587 'To create request elements, you must provide credentials '
1589 1588 'and URL either as keyword arguments or in the JSON object!')
1590 1589
1591 1590 # Get the provider class
1592 1591 credentials = Credentials.deserialize(self.config, credentials)
1593 1592 ProviderClass = credentials.provider_class
1594 1593
1595 1594 # Create request elements
1596 1595 request_elements = ProviderClass.create_request_elements(
1597 1596 ProviderClass.PROTECTED_RESOURCE_REQUEST_TYPE,
1598 1597 credentials=credentials,
1599 1598 url=url,
1600 1599 method=method,
1601 1600 params=params,
1602 1601 headers=headers,
1603 1602 body=body)
1604 1603
1605 1604 if return_json:
1606 1605 return request_elements.to_json()
1607 1606
1608 1607 else:
1609 1608 return request_elements
1610 1609
1611 1610 def backend(self, adapter):
1612 1611 """
1613 1612 Converts a *request handler* to a JSON backend which you can use with
1614 1613 :ref:`authomatic.js <js>`.
1615 1614
1616 1615 Just call it inside a *request handler* like this:
1617 1616
1618 1617 ::
1619 1618
1620 1619 class JSONHandler(webapp2.RequestHandler):
1621 1620 def get(self):
1622 1621 authomatic.backend(Webapp2Adapter(self))
1623 1622
1624 1623 :param adapter:
1625 1624 The only argument is an :doc:`adapter <adapters>`.
1626 1625
1627 1626 The *request handler* will now accept these request parameters:
1628 1627
1629 1628 :param str type:
1630 1629 Type of the request. Either ``auto``, ``fetch`` or ``elements``.
1631 1630 Default is ``auto``.
1632 1631
1633 1632 :param str credentials:
1634 1633 Serialized :class:`.Credentials`.
1635 1634
1636 1635 :param str url:
1637 1636 URL of the **protected resource** request.
1638 1637
1639 1638 :param str method:
1640 1639 HTTP method of the **protected resource** request.
1641 1640
1642 1641 :param str body:
1643 1642 HTTP body of the **protected resource** request.
1644 1643
1645 1644 :param JSON params:
1646 1645 HTTP params of the **protected resource** request as a JSON object.
1647 1646
1648 1647 :param JSON headers:
1649 1648 HTTP headers of the **protected resource** request as a
1650 1649 JSON object.
1651 1650
1652 1651 :param JSON json:
1653 1652 You can pass all of the aforementioned params except ``type``
1654 1653 in a JSON object.
1655 1654
1656 1655 .. code-block:: javascript
1657 1656
1658 1657 {
1659 1658 "credentials": "######",
1660 1659 "url": "https://example.com",
1661 1660 "method": "POST",
1662 1661 "params": {"foo": "bar"},
1663 1662 "headers": {"baz": "bing"},
1664 1663 "body": "the body of the request"
1665 1664 }
1666 1665
1667 1666 Depending on the ``type`` param, the handler will either write
1668 1667 a JSON object with *request elements* to the response,
1669 1668 and add an ``Authomatic-Response-To: elements`` response header, ...
1670 1669
1671 1670 .. code-block:: javascript
1672 1671
1673 1672 {
1674 1673 "url": "https://example.com/api",
1675 1674 "method": "POST",
1676 1675 "params": {
1677 1676 "access_token": "###",
1678 1677 "foo": "bar"
1679 1678 },
1680 1679 "headers": {
1681 1680 "baz": "bing",
1682 1681 "Authorization": "Bearer ###"
1683 1682 }
1684 1683 }
1685 1684
1686 1685 ... or make a fetch to the **protected resource** and forward
1687 1686 it's response content, status and headers with an additional
1688 1687 ``Authomatic-Response-To: fetch`` header to the response.
1689 1688
1690 1689 .. warning::
1691 1690
1692 1691 The backend will not work if you write anything to the
1693 1692 response in the handler!
1694 1693
1695 1694 """
1696 1695
1697 1696 AUTHOMATIC_HEADER = 'Authomatic-Response-To'
1698 1697
1699 1698 # Collect request params
1700 1699 request_type = adapter.params.get('type', 'auto')
1701 1700 json_input = adapter.params.get('json')
1702 1701 credentials = adapter.params.get('credentials')
1703 1702 url = adapter.params.get('url')
1704 1703 method = adapter.params.get('method', 'GET')
1705 1704 body = adapter.params.get('body', '')
1706 1705
1707 1706 params = adapter.params.get('params')
1708 1707 params = json.loads(params) if params else {}
1709 1708
1710 1709 headers = adapter.params.get('headers')
1711 1710 headers = json.loads(headers) if headers else {}
1712 1711
1713 1712 ProviderClass = Credentials.deserialize(
1714 1713 self.config, credentials).provider_class
1715 1714
1716 1715 if request_type == 'auto':
1717 1716 # If there is a "callback" param, it's a JSONP request.
1718 1717 jsonp = params.get('callback')
1719 1718
1720 1719 # JSONP is possible only with GET method.
1721 1720 if ProviderClass.supports_jsonp and method == 'GET':
1722 1721 request_type = 'elements'
1723 1722 else:
1724 1723 # Remove the JSONP callback
1725 1724 if jsonp:
1726 1725 params.pop('callback')
1727 1726 request_type = 'fetch'
1728 1727
1729 1728 if request_type == 'fetch':
1730 1729 # Access protected resource
1731 1730 response = self.access(
1732 1731 credentials, url, params, method, headers, body)
1733 1732 result = response.content
1734 1733
1735 1734 # Forward status
1736 1735 adapter.status = str(response.status) + ' ' + str(response.reason)
1737 1736
1738 1737 # Forward headers
1739 1738 for k, v in response.getheaders():
1740 1739 logging.info(' {0}: {1}'.format(k, v))
1741 1740 adapter.set_header(k, v)
1742 1741
1743 1742 elif request_type == 'elements':
1744 1743 # Create request elements
1745 1744 if json_input:
1746 1745 result = self.request_elements(
1747 1746 json_input=json_input, return_json=True)
1748 1747 else:
1749 1748 result = self.request_elements(credentials=credentials,
1750 1749 url=url,
1751 1750 method=method,
1752 1751 params=params,
1753 1752 headers=headers,
1754 1753 body=body,
1755 1754 return_json=True)
1756 1755
1757 1756 adapter.set_header('Content-Type', 'application/json')
1758 1757 else:
1759 1758 result = '{"error": "Bad Request!"}'
1760 1759
1761 1760 # Add the authomatic header
1762 1761 adapter.set_header(AUTHOMATIC_HEADER, request_type)
1763 1762
1764 1763 # Write result to response
1765 1764 adapter.write(result)
General Comments 0
You need to be logged in to leave comments. Login now