Statistics
| Revision:

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)