gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / requests_oauthlib / oauth2_session.py @ 564
History | View | Annotate | Download (15.6 KB)
1 |
from __future__ import unicode_literals |
---|---|
2 |
|
3 |
import logging |
4 |
|
5 |
from oauthlib.common import generate_token, urldecode |
6 |
from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError |
7 |
from oauthlib.oauth2 import TokenExpiredError, is_secure_transport |
8 |
import requests |
9 |
|
10 |
log = logging.getLogger(__name__) |
11 |
|
12 |
|
13 |
class TokenUpdated(Warning): |
14 |
def __init__(self, token): |
15 |
super(TokenUpdated, self).__init__() |
16 |
self.token = token
|
17 |
|
18 |
|
19 |
class OAuth2Session(requests.Session): |
20 |
"""Versatile OAuth 2 extension to :class:`requests.Session`.
|
21 |
|
22 |
Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec
|
23 |
including the four core OAuth 2 grants.
|
24 |
|
25 |
Can be used to create authorization urls, fetch tokens and access protected
|
26 |
resources using the :class:`requests.Session` interface you are used to.
|
27 |
|
28 |
- :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant
|
29 |
- :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant
|
30 |
- :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant
|
31 |
- :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant
|
32 |
|
33 |
Note that the only time you will be using Implicit Grant from python is if
|
34 |
you are driving a user agent able to obtain URL fragments.
|
35 |
"""
|
36 |
|
37 |
def __init__(self, client_id=None, client=None, auto_refresh_url=None, |
38 |
auto_refresh_kwargs=None, scope=None, redirect_uri=None, token=None, |
39 |
state=None, token_updater=None, **kwargs): |
40 |
"""Construct a new OAuth 2 client session.
|
41 |
|
42 |
:param client_id: Client id obtained during registration
|
43 |
:param client: :class:`oauthlib.oauth2.Client` to be used. Default is
|
44 |
WebApplicationClient which is useful for any
|
45 |
hosted application but not mobile or desktop.
|
46 |
:param scope: List of scopes you wish to request access to
|
47 |
:param redirect_uri: Redirect URI you registered as callback
|
48 |
:param token: Token dictionary, must include access_token
|
49 |
and token_type.
|
50 |
:param state: State string used to prevent CSRF. This will be given
|
51 |
when creating the authorization url and must be supplied
|
52 |
when parsing the authorization response.
|
53 |
Can be either a string or a no argument callable.
|
54 |
:auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply
|
55 |
this if you wish the client to automatically refresh
|
56 |
your access tokens.
|
57 |
:auto_refresh_kwargs: Extra arguments to pass to the refresh token
|
58 |
endpoint.
|
59 |
:token_updater: Method with one argument, token, to be used to update
|
60 |
your token databse on automatic token refresh. If not
|
61 |
set a TokenUpdated warning will be raised when a token
|
62 |
has been refreshed. This warning will carry the token
|
63 |
in its token argument.
|
64 |
:param kwargs: Arguments to pass to the Session constructor.
|
65 |
"""
|
66 |
super(OAuth2Session, self).__init__(**kwargs) |
67 |
self._client = client or WebApplicationClient(client_id, token=token) |
68 |
self.token = token or {} |
69 |
self.scope = scope
|
70 |
self.redirect_uri = redirect_uri
|
71 |
self.state = state or generate_token |
72 |
self._state = state
|
73 |
self.auto_refresh_url = auto_refresh_url
|
74 |
self.auto_refresh_kwargs = auto_refresh_kwargs or {} |
75 |
self.token_updater = token_updater
|
76 |
|
77 |
# Allow customizations for non compliant providers through various
|
78 |
# hooks to adjust requests and responses.
|
79 |
self.compliance_hook = {
|
80 |
'access_token_response': set([]), |
81 |
'refresh_token_response': set([]), |
82 |
'protected_request': set([]), |
83 |
} |
84 |
|
85 |
def new_state(self): |
86 |
"""Generates a state string to be used in authorizations."""
|
87 |
try:
|
88 |
self._state = self.state() |
89 |
log.debug('Generated new state %s.', self._state) |
90 |
except TypeError: |
91 |
self._state = self.state |
92 |
log.debug('Re-using previously supplied state %s.', self._state) |
93 |
return self._state |
94 |
|
95 |
@property
|
96 |
def client_id(self): |
97 |
return getattr(self._client, "client_id", None) |
98 |
|
99 |
@client_id.setter
|
100 |
def client_id(self, value): |
101 |
self._client.client_id = value
|
102 |
|
103 |
@client_id.deleter
|
104 |
def client_id(self): |
105 |
del self._client.client_id |
106 |
|
107 |
@property
|
108 |
def token(self): |
109 |
return getattr(self._client, "token", None) |
110 |
|
111 |
@token.setter
|
112 |
def token(self, value): |
113 |
self._client.token = value
|
114 |
self._client._populate_attributes(value)
|
115 |
|
116 |
@property
|
117 |
def access_token(self): |
118 |
return getattr(self._client, "access_token", None) |
119 |
|
120 |
@access_token.setter
|
121 |
def access_token(self, value): |
122 |
self._client.access_token = value
|
123 |
|
124 |
@access_token.deleter
|
125 |
def access_token(self): |
126 |
del self._client.access_token |
127 |
|
128 |
@property
|
129 |
def authorized(self): |
130 |
"""Boolean that indicates whether this session has an OAuth token
|
131 |
or not. If `self.authorized` is True, you can reasonably expect
|
132 |
OAuth-protected requests to the resource to succeed. If
|
133 |
`self.authorized` is False, you need the user to go through the OAuth
|
134 |
authentication dance before OAuth-protected requests to the resource
|
135 |
will succeed.
|
136 |
"""
|
137 |
return bool(self.access_token) |
138 |
|
139 |
def authorization_url(self, url, state=None, **kwargs): |
140 |
"""Form an authorization URL.
|
141 |
|
142 |
:param url: Authorization endpoint url, must be HTTPS.
|
143 |
:param state: An optional state string for CSRF protection. If not
|
144 |
given it will be generated for you.
|
145 |
:param kwargs: Extra parameters to include.
|
146 |
:return: authorization_url, state
|
147 |
"""
|
148 |
state = state or self.new_state() |
149 |
return self._client.prepare_request_uri(url, |
150 |
redirect_uri=self.redirect_uri,
|
151 |
scope=self.scope,
|
152 |
state=state, |
153 |
**kwargs), state |
154 |
|
155 |
def fetch_token(self, token_url, code=None, authorization_response=None, |
156 |
body='', auth=None, username=None, password=None, method='POST', |
157 |
timeout=None, headers=None, verify=True, **kwargs): |
158 |
"""Generic method for fetching an access token from the token endpoint.
|
159 |
|
160 |
If you are using the MobileApplicationClient you will want to use
|
161 |
token_from_fragment instead of fetch_token.
|
162 |
|
163 |
:param token_url: Token endpoint URL, must use HTTPS.
|
164 |
:param code: Authorization code (used by WebApplicationClients).
|
165 |
:param authorization_response: Authorization response URL, the callback
|
166 |
URL of the request back to you. Used by
|
167 |
WebApplicationClients instead of code.
|
168 |
:param body: Optional application/x-www-form-urlencoded body to add the
|
169 |
include in the token request. Prefer kwargs over body.
|
170 |
:param auth: An auth tuple or method as accepted by requests.
|
171 |
:param username: Username used by LegacyApplicationClients.
|
172 |
:param password: Password used by LegacyApplicationClients.
|
173 |
:param method: The HTTP method used to make the request. Defaults
|
174 |
to POST, but may also be GET. Other methods should
|
175 |
be added as needed.
|
176 |
:param headers: Dict to default request headers with.
|
177 |
:param timeout: Timeout of the request in seconds.
|
178 |
:param verify: Verify SSL certificate.
|
179 |
:param kwargs: Extra parameters to include in the token request.
|
180 |
:return: A token dict
|
181 |
"""
|
182 |
if not is_secure_transport(token_url): |
183 |
raise InsecureTransportError()
|
184 |
|
185 |
if not code and authorization_response: |
186 |
self._client.parse_request_uri_response(authorization_response,
|
187 |
state=self._state)
|
188 |
code = self._client.code
|
189 |
elif not code and isinstance(self._client, WebApplicationClient): |
190 |
code = self._client.code
|
191 |
if not code: |
192 |
raise ValueError('Please supply either code or ' |
193 |
'authorization_code parameters.')
|
194 |
|
195 |
|
196 |
body = self._client.prepare_request_body(code=code, body=body,
|
197 |
redirect_uri=self.redirect_uri, username=username,
|
198 |
password=password, **kwargs) |
199 |
|
200 |
auth = auth or requests.auth.HTTPBasicAuth(username, password)
|
201 |
|
202 |
headers = headers or {
|
203 |
'Accept': 'application/json', |
204 |
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', |
205 |
} |
206 |
if method.upper() == 'POST': |
207 |
r = self.post(token_url, data=dict(urldecode(body)), |
208 |
timeout=timeout, headers=headers, auth=auth, |
209 |
verify=verify) |
210 |
log.debug('Prepared fetch token request body %s', body)
|
211 |
elif method.upper() == 'GET': |
212 |
# if method is not 'POST', switch body to querystring and GET
|
213 |
r = self.get(token_url, params=dict(urldecode(body)), |
214 |
timeout=timeout, headers=headers, auth=auth, |
215 |
verify=verify) |
216 |
log.debug('Prepared fetch token request querystring %s', body)
|
217 |
else:
|
218 |
raise ValueError('The method kwarg must be POST or GET.') |
219 |
|
220 |
log.debug('Request to fetch token completed with status %s.',
|
221 |
r.status_code) |
222 |
log.debug('Request headers were %s', r.request.headers)
|
223 |
log.debug('Request body was %s', r.request.body)
|
224 |
log.debug('Response headers were %s and content %s.',
|
225 |
r.headers, r.text) |
226 |
log.debug('Invoking %d token response hooks.',
|
227 |
len(self.compliance_hook['access_token_response'])) |
228 |
for hook in self.compliance_hook['access_token_response']: |
229 |
log.debug('Invoking hook %s.', hook)
|
230 |
r = hook(r) |
231 |
|
232 |
self._client.parse_request_body_response(r.text, scope=self.scope) |
233 |
self.token = self._client.token |
234 |
log.debug('Obtained token %s.', self.token) |
235 |
return self.token |
236 |
|
237 |
def token_from_fragment(self, authorization_response): |
238 |
"""Parse token from the URI fragment, used by MobileApplicationClients.
|
239 |
|
240 |
:param authorization_response: The full URL of the redirect back to you
|
241 |
:return: A token dict
|
242 |
"""
|
243 |
self._client.parse_request_uri_response(authorization_response,
|
244 |
state=self._state)
|
245 |
self.token = self._client.token |
246 |
return self.token |
247 |
|
248 |
def refresh_token(self, token_url, refresh_token=None, body='', auth=None, |
249 |
timeout=None, headers=None, verify=True, **kwargs): |
250 |
"""Fetch a new access token using a refresh token.
|
251 |
|
252 |
:param token_url: The token endpoint, must be HTTPS.
|
253 |
:param refresh_token: The refresh_token to use.
|
254 |
:param body: Optional application/x-www-form-urlencoded body to add the
|
255 |
include in the token request. Prefer kwargs over body.
|
256 |
:param auth: An auth tuple or method as accepted by requests.
|
257 |
:param timeout: Timeout of the request in seconds.
|
258 |
:param verify: Verify SSL certificate.
|
259 |
:param kwargs: Extra parameters to include in the token request.
|
260 |
:return: A token dict
|
261 |
"""
|
262 |
if not token_url: |
263 |
raise ValueError('No token endpoint set for auto_refresh.') |
264 |
|
265 |
if not is_secure_transport(token_url): |
266 |
raise InsecureTransportError()
|
267 |
|
268 |
# Need to nullify token to prevent it from being added to the request
|
269 |
refresh_token = refresh_token or self.token.get('refresh_token') |
270 |
self.token = {}
|
271 |
|
272 |
log.debug('Adding auto refresh key word arguments %s.',
|
273 |
self.auto_refresh_kwargs)
|
274 |
kwargs.update(self.auto_refresh_kwargs)
|
275 |
body = self._client.prepare_refresh_body(body=body,
|
276 |
refresh_token=refresh_token, scope=self.scope, **kwargs)
|
277 |
log.debug('Prepared refresh token request body %s', body)
|
278 |
|
279 |
if headers is None: |
280 |
headers = { |
281 |
'Accept': 'application/json', |
282 |
'Content-Type': (
|
283 |
'application/x-www-form-urlencoded;charset=UTF-8'
|
284 |
), |
285 |
} |
286 |
|
287 |
r = self.post(token_url, data=dict(urldecode(body)), auth=auth, |
288 |
timeout=timeout, headers=headers, verify=verify) |
289 |
log.debug('Request to refresh token completed with status %s.',
|
290 |
r.status_code) |
291 |
log.debug('Response headers were %s and content %s.',
|
292 |
r.headers, r.text) |
293 |
log.debug('Invoking %d token response hooks.',
|
294 |
len(self.compliance_hook['refresh_token_response'])) |
295 |
for hook in self.compliance_hook['refresh_token_response']: |
296 |
log.debug('Invoking hook %s.', hook)
|
297 |
r = hook(r) |
298 |
|
299 |
self.token = self._client.parse_request_body_response(r.text, scope=self.scope) |
300 |
if not 'refresh_token' in self.token: |
301 |
log.debug('No new refresh token given. Re-using old.')
|
302 |
self.token['refresh_token'] = refresh_token |
303 |
return self.token |
304 |
|
305 |
def request(self, method, url, data=None, headers=None, **kwargs): |
306 |
"""Intercept all requests and add the OAuth 2 token if present."""
|
307 |
if not is_secure_transport(url): |
308 |
raise InsecureTransportError()
|
309 |
if self.token: |
310 |
log.debug('Invoking %d protected resource request hooks.',
|
311 |
len(self.compliance_hook['protected_request'])) |
312 |
for hook in self.compliance_hook['protected_request']: |
313 |
log.debug('Invoking hook %s.', hook)
|
314 |
url, headers, data = hook(url, headers, data) |
315 |
|
316 |
log.debug('Adding token %s to request.', self.token) |
317 |
try:
|
318 |
url, headers, data = self._client.add_token(url,
|
319 |
http_method=method, body=data, headers=headers) |
320 |
# Attempt to retrieve and save new access token if expired
|
321 |
except TokenExpiredError:
|
322 |
if self.auto_refresh_url: |
323 |
log.debug('Auto refresh is set, attempting to refresh at %s.',
|
324 |
self.auto_refresh_url)
|
325 |
token = self.refresh_token(self.auto_refresh_url, **kwargs) |
326 |
if self.token_updater: |
327 |
log.debug('Updating token to %s using %s.',
|
328 |
token, self.token_updater)
|
329 |
self.token_updater(token)
|
330 |
url, headers, data = self._client.add_token(url,
|
331 |
http_method=method, body=data, headers=headers) |
332 |
else:
|
333 |
raise TokenUpdated(token)
|
334 |
else:
|
335 |
raise
|
336 |
|
337 |
log.debug('Requesting url %s using method %s.', url, method)
|
338 |
log.debug('Supplying headers %s and data %s', headers, data)
|
339 |
log.debug('Passing through key word arguments %s.', kwargs)
|
340 |
return super(OAuth2Session, self).request(method, url, |
341 |
headers=headers, data=data, **kwargs) |
342 |
|
343 |
def register_compliance_hook(self, hook_type, hook): |
344 |
"""Register a hook for request/response tweaking.
|
345 |
|
346 |
Available hooks are:
|
347 |
access_token_response invoked before token parsing.
|
348 |
refresh_token_response invoked before refresh token parsing.
|
349 |
protected_request invoked before making a request.
|
350 |
|
351 |
If you find a new hook is needed please send a GitHub PR request
|
352 |
or open an issue.
|
353 |
"""
|
354 |
if hook_type not in self.compliance_hook: |
355 |
raise ValueError('Hook type %s is not in %s.', |
356 |
hook_type, self.compliance_hook)
|
357 |
self.compliance_hook[hook_type].add(hook)
|