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