summaryrefslogtreecommitdiff
path: root/oauthlib/oauth2/rfc6749/clients/base.py
blob: 04dabe6577555d39d0e4ec76e353cb6eed00024f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~

This module is an implementation of various logic needed
for consuming OAuth 2.0 RFC6749.
"""
import time
import warnings

from oauthlib.common import generate_token
from oauthlib.oauth2.rfc6749 import tokens
from oauthlib.oauth2.rfc6749.errors import (InsecureTransportError,
                                            TokenExpiredError)
from oauthlib.oauth2.rfc6749.parameters import (parse_token_response,
                                                prepare_token_request,
                                                prepare_token_revocation_request)
from oauthlib.oauth2.rfc6749.utils import is_secure_transport

AUTH_HEADER = 'auth_header'
URI_QUERY = 'query'
BODY = 'body'

FORM_ENC_HEADERS = {
    'Content-Type': 'application/x-www-form-urlencoded'
}


class Client:
    """Base OAuth2 client responsible for access token management.

    This class also acts as a generic interface providing methods common to all
    client types such as ``prepare_authorization_request`` and
    ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
    the recommended way of interacting with clients (as opposed to the abstract
    prepare uri/body/etc methods). They are recommended over the older set
    because they are easier to use (more consistent) and add a few additional
    security checks, such as HTTPS and state checking.

    Some of these methods require further implementation only provided by the
    specific purpose clients such as
    :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
    seek to use the client class matching the OAuth workflow you need. For
    Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.

    """
    refresh_token_key = 'refresh_token'

    def __init__(self, client_id,
                 default_token_placement=AUTH_HEADER,
                 token_type='Bearer',
                 access_token=None,
                 refresh_token=None,
                 mac_key=None,
                 mac_algorithm=None,
                 token=None,
                 scope=None,
                 state=None,
                 redirect_url=None,
                 state_generator=generate_token,
                 **kwargs):
        """Initialize a client with commonly used attributes.

        :param client_id: Client identifier given by the OAuth provider upon
        registration.

        :param default_token_placement: Tokens can be supplied in the Authorization
        header (default), the URL query component (``query``) or the request
        body (``body``).

        :param token_type: OAuth 2 token type. Defaults to Bearer. Change this
        if you specify the ``access_token`` parameter and know it is of a
        different token type, such as a MAC, JWT or SAML token. Can
        also be supplied as ``token_type`` inside the ``token`` dict parameter.

        :param access_token: An access token (string) used to authenticate
        requests to protected resources. Can also be supplied inside the
        ``token`` dict parameter.

        :param refresh_token: A refresh token (string) used to refresh expired
        tokens. Can also be supplied inside the ``token`` dict parameter.

        :param mac_key: Encryption key used with MAC tokens.

        :param mac_algorithm:  Hashing algorithm for MAC tokens.

        :param token: A dict of token attributes such as ``access_token``,
        ``token_type`` and ``expires_at``.

        :param scope: A list of default scopes to request authorization for.

        :param state: A CSRF protection string used during authorization.

        :param redirect_url: The redirection endpoint on the client side to which
        the user returns after authorization.

        :param state_generator: A no argument state generation callable. Defaults
        to :py:meth:`oauthlib.common.generate_token`.
        """

        self.client_id = client_id
        self.default_token_placement = default_token_placement
        self.token_type = token_type
        self.access_token = access_token
        self.refresh_token = refresh_token
        self.mac_key = mac_key
        self.mac_algorithm = mac_algorithm
        self.token = token or {}
        self.scope = scope
        self.state_generator = state_generator
        self.state = state
        self.redirect_url = redirect_url
        self.code = None
        self.expires_in = None
        self._expires_at = None
        self.populate_token_attributes(self.token)

    @property
    def token_types(self):
        """Supported token types and their respective methods

        Additional tokens can be supported by extending this dictionary.

        The Bearer token spec is stable and safe to use.

        The MAC token spec is not yet stable and support for MAC tokens
        is experimental and currently matching version 00 of the spec.
        """
        return {
            'Bearer': self._add_bearer_token,
            'MAC': self._add_mac_token
        }

    def prepare_request_uri(self, *args, **kwargs):
        """Abstract method used to create request URIs."""
        raise NotImplementedError("Must be implemented by inheriting classes.")

    def prepare_request_body(self, *args, **kwargs):
        """Abstract method used to create request bodies."""
        raise NotImplementedError("Must be implemented by inheriting classes.")

    def parse_request_uri_response(self, *args, **kwargs):
        """Abstract method used to parse redirection responses."""
        raise NotImplementedError("Must be implemented by inheriting classes.")

    def add_token(self, uri, http_method='GET', body=None, headers=None,
                  token_placement=None, **kwargs):
        """Add token to the request uri, body or authorization header.

        The access token type provides the client with the information
        required to successfully utilize the access token to make a protected
        resource request (along with type-specific attributes).  The client
        MUST NOT use an access token if it does not understand the token
        type.

        For example, the "bearer" token type defined in
        [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
        token string in the request:

        .. code-block:: http

            GET /resource/1 HTTP/1.1
            Host: example.com
            Authorization: Bearer mF_9.B5f-4.1JqM

        while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
        utilized by issuing a MAC key together with the access token which is
        used to sign certain components of the HTTP requests:

        .. code-block:: http

            GET /resource/1 HTTP/1.1
            Host: example.com
            Authorization: MAC id="h480djs93hd8",
                                nonce="274312:dj83hs9s",
                                mac="kDZvddkndxvhGRXZhvuDjEWhGeE="

        .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2
        .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2
        """
        if not is_secure_transport(uri):
            raise InsecureTransportError()

        token_placement = token_placement or self.default_token_placement

        case_insensitive_token_types = {
            k.lower(): v for k, v in self.token_types.items()}
        if not self.token_type.lower() in case_insensitive_token_types:
            raise ValueError("Unsupported token type: %s" % self.token_type)

        if not (self.access_token or self.token.get('access_token')):
            raise ValueError("Missing access token.")

        if self._expires_at and self._expires_at < time.time():
            raise TokenExpiredError()

        return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
                                                                     headers, token_placement, **kwargs)

    def prepare_authorization_request(self, authorization_url, state=None,
                                      redirect_url=None, scope=None, **kwargs):
        """Prepare the authorization request.

        This is the first step in many OAuth flows in which the user is
        redirected to a certain authorization URL. This method adds
        required parameters to the authorization URL.

        :param authorization_url: Provider authorization endpoint URL.

        :param state: CSRF protection string. Will be automatically created if
        not provided. The generated state is available via the ``state``
        attribute. Clients should verify that the state is unchanged and
        present in the authorization response. This verification is done
        automatically if using the ``authorization_response`` parameter
        with ``prepare_token_request``.

        :param redirect_url: Redirect URL to which the user will be returned
        after authorization. Must be provided unless previously setup with
        the provider. If provided then it must also be provided in the
        token request.

        :param scope:

        :param kwargs: Additional parameters to included in the request.

        :returns: The prepared request tuple with (url, headers, body).
        """
        if not is_secure_transport(authorization_url):
            raise InsecureTransportError()

        self.state = state or self.state_generator()
        self.redirect_url = redirect_url or self.redirect_url
        self.scope = scope or self.scope
        auth_url = self.prepare_request_uri(
            authorization_url, redirect_uri=self.redirect_url,
            scope=self.scope, state=self.state, **kwargs)
        return auth_url, FORM_ENC_HEADERS, ''

    def prepare_token_request(self, token_url, authorization_response=None,
                              redirect_url=None, state=None, body='', **kwargs):
        """Prepare a token creation request.

        Note that these requests usually require client authentication, either
        by including client_id or a set of provider specific authentication
        credentials.

        :param token_url: Provider token creation endpoint URL.

        :param authorization_response: The full redirection URL string, i.e.
        the location to which the user was redirected after successfull
        authorization. Used to mine credentials needed to obtain a token
        in this step, such as authorization code.

        :param redirect_url: The redirect_url supplied with the authorization
        request (if there was one).

        :param state:

        :param body: Existing request body (URL encoded string) to embed parameters
                     into. This may contain extra paramters. Default ''.

        :param kwargs: Additional parameters to included in the request.

        :returns: The prepared request tuple with (url, headers, body).
        """
        if not is_secure_transport(token_url):
            raise InsecureTransportError()

        state = state or self.state
        if authorization_response:
            self.parse_request_uri_response(
                authorization_response, state=state)
        self.redirect_url = redirect_url or self.redirect_url
        body = self.prepare_request_body(body=body,
                                         redirect_uri=self.redirect_url, **kwargs)

        return token_url, FORM_ENC_HEADERS, body

    def prepare_refresh_token_request(self, token_url, refresh_token=None,
                                      body='', scope=None, **kwargs):
        """Prepare an access token refresh request.

        Expired access tokens can be replaced by new access tokens without
        going through the OAuth dance if the client obtained a refresh token.
        This refresh token and authentication credentials can be used to
        obtain a new access token, and possibly a new refresh token.

        :param token_url: Provider token refresh endpoint URL.

        :param refresh_token: Refresh token string.

        :param body: Existing request body (URL encoded string) to embed parameters
                     into. This may contain extra paramters. Default ''.

        :param scope: List of scopes to request. Must be equal to
        or a subset of the scopes granted when obtaining the refresh
        token.

        :param kwargs: Additional parameters to included in the request.

        :returns: The prepared request tuple with (url, headers, body).
        """
        if not is_secure_transport(token_url):
            raise InsecureTransportError()

        self.scope = scope or self.scope
        body = self.prepare_refresh_body(body=body,
                                         refresh_token=refresh_token, scope=self.scope, **kwargs)
        return token_url, FORM_ENC_HEADERS, body

    def prepare_token_revocation_request(self, revocation_url, token,
                                         token_type_hint="access_token", body='', callback=None, **kwargs):
        """Prepare a token revocation request.

        :param revocation_url: Provider token revocation endpoint URL.

        :param token: The access or refresh token to be revoked (string).

        :param token_type_hint: ``"access_token"`` (default) or
        ``"refresh_token"``. This is optional and if you wish to not pass it you
        must provide ``token_type_hint=None``.

        :param body:

        :param callback: A jsonp callback such as ``package.callback`` to be invoked
        upon receiving the response. Not that it should not include a () suffix.

        :param kwargs: Additional parameters to included in the request.

        :returns: The prepared request tuple with (url, headers, body).

        Note that JSONP request may use GET requests as the parameters will
        be added to the request URL query as opposed to the request body.

        An example of a revocation request

        .. code-block: http

            POST /revoke HTTP/1.1
            Host: server.example.com
            Content-Type: application/x-www-form-urlencoded
            Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

            token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token

        An example of a jsonp revocation request

        .. code-block: http

            GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
            Host: server.example.com
            Content-Type: application/x-www-form-urlencoded
            Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

        and an error response

        .. code-block: http

        package.myCallback({"error":"unsupported_token_type"});

        Note that these requests usually require client credentials, client_id in
        the case for public clients and provider specific authentication
        credentials for confidential clients.
        """
        if not is_secure_transport(revocation_url):
            raise InsecureTransportError()

        return prepare_token_revocation_request(revocation_url, token,
                                                token_type_hint=token_type_hint, body=body, callback=callback,
                                                **kwargs)

    def parse_request_body_response(self, body, scope=None, **kwargs):
        """Parse the JSON response body.

        If the access token request is valid and authorized, the
        authorization server issues an access token as described in
        `Section 5.1`_.  A refresh token SHOULD NOT be included.  If the request
        failed client authentication or is invalid, the authorization server
        returns an error response as described in `Section 5.2`_.

        :param body: The response body from the token request.
        :param scope: Scopes originally requested.
        :return: Dictionary of token parameters.
        :raises: Warning if scope has changed. OAuth2Error if response is invalid.

        These response are json encoded and could easily be parsed without
        the assistance of OAuthLib. However, there are a few subtle issues
        to be aware of regarding the response which are helpfully addressed
        through the raising of various errors.

        A successful response should always contain

        **access_token**
                The access token issued by the authorization server. Often
                a random string.

        **token_type**
            The type of the token issued as described in `Section 7.1`_.
            Commonly ``Bearer``.

        While it is not mandated it is recommended that the provider include

        **expires_in**
            The lifetime in seconds of the access token.  For
            example, the value "3600" denotes that the access token will
            expire in one hour from the time the response was generated.
            If omitted, the authorization server SHOULD provide the
            expiration time via other means or document the default value.

           **scope**
            Providers may supply this in all responses but are required to only
            if it has changed since the authorization request.

        .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
        .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
        .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
        """
        self.token = parse_token_response(body, scope=scope)
        self.populate_token_attributes(self.token)
        return self.token

    def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
        """Prepare an access token request, using a refresh token.

        If the authorization server issued a refresh token to the client, the
        client makes a refresh request to the token endpoint by adding the
        following parameters using the "application/x-www-form-urlencoded"
        format in the HTTP request entity-body:

        grant_type
                REQUIRED.  Value MUST be set to "refresh_token".
        refresh_token
                REQUIRED.  The refresh token issued to the client.
        scope
                OPTIONAL.  The scope of the access request as described by
                Section 3.3.  The requested scope MUST NOT include any scope
                not originally granted by the resource owner, and if omitted is
                treated as equal to the scope originally granted by the
                resource owner.
        """
        refresh_token = refresh_token or self.refresh_token
        return prepare_token_request(self.refresh_token_key, body=body, scope=scope,
                                     refresh_token=refresh_token, **kwargs)

    def _add_bearer_token(self, uri, http_method='GET', body=None,
                          headers=None, token_placement=None):
        """Add a bearer token to the request uri, body or authorization header."""
        if token_placement == AUTH_HEADER:
            headers = tokens.prepare_bearer_headers(self.access_token, headers)

        elif token_placement == URI_QUERY:
            uri = tokens.prepare_bearer_uri(self.access_token, uri)

        elif token_placement == BODY:
            body = tokens.prepare_bearer_body(self.access_token, body)

        else:
            raise ValueError("Invalid token placement.")
        return uri, headers, body

    def _add_mac_token(self, uri, http_method='GET', body=None,
                       headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
        """Add a MAC token to the request authorization header.

        Warning: MAC token support is experimental as the spec is not yet stable.
        """
        if token_placement != AUTH_HEADER:
            raise ValueError("Invalid token placement.")

        headers = tokens.prepare_mac_header(self.access_token, uri,
                                            self.mac_key, http_method, headers=headers, body=body, ext=ext,
                                            hash_algorithm=self.mac_algorithm, **kwargs)
        return uri, headers, body

    def _populate_attributes(self, response):
        warnings.warn("Please switch to the public method "
                      "populate_token_attributes.", DeprecationWarning)
        return self.populate_token_attributes(response)

    def populate_code_attributes(self, response):
        """Add attributes from an auth code response to self."""

        if 'code' in response:
            self.code = response.get('code')

    def populate_token_attributes(self, response):
        """Add attributes from a token exchange response to self."""

        if 'access_token' in response:
            self.access_token = response.get('access_token')

        if 'refresh_token' in response:
            self.refresh_token = response.get('refresh_token')

        if 'token_type' in response:
            self.token_type = response.get('token_type')

        if 'expires_in' in response:
            self.expires_in = response.get('expires_in')
            self._expires_at = time.time() + int(self.expires_in)

        if 'expires_at' in response:
            self._expires_at = int(response.get('expires_at'))

        if 'mac_key' in response:
            self.mac_key = response.get('mac_key')

        if 'mac_algorithm' in response:
            self.mac_algorithm = response.get('mac_algorithm')