1# -*- coding: utf-8 -*-
2"""
3oauthlib.oauth2.rfc6749
4~~~~~~~~~~~~~~~~~~~~~~~
5
6This module is an implementation of various logic needed
7for consuming OAuth 2.0 RFC6749.
8"""
9import base64
10import hashlib
11import time
12import warnings
13
14from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_token
15from oauthlib.oauth2.rfc6749 import tokens
16from oauthlib.oauth2.rfc6749.errors import (
17 InsecureTransportError, TokenExpiredError,
18)
19from oauthlib.oauth2.rfc6749.parameters import (
20 parse_expires,
21 parse_token_response, prepare_token_request,
22 prepare_token_revocation_request,
23)
24from oauthlib.oauth2.rfc6749.utils import is_secure_transport
25
26AUTH_HEADER = 'auth_header'
27URI_QUERY = 'query'
28BODY = 'body'
29
30FORM_ENC_HEADERS = {
31 'Content-Type': 'application/x-www-form-urlencoded'
32}
33
34
35class Client:
36 """Base OAuth2 client responsible for access token management.
37
38 This class also acts as a generic interface providing methods common to all
39 client types such as ``prepare_authorization_request`` and
40 ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
41 the recommended way of interacting with clients (as opposed to the abstract
42 prepare uri/body/etc methods). They are recommended over the older set
43 because they are easier to use (more consistent) and add a few additional
44 security checks, such as HTTPS and state checking.
45
46 Some of these methods require further implementation only provided by the
47 specific purpose clients such as
48 :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
49 seek to use the client class matching the OAuth workflow you need. For
50 Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
51
52 """
53 refresh_token_key = 'refresh_token'
54
55 def __init__(self, client_id,
56 default_token_placement=AUTH_HEADER,
57 token_type='Bearer',
58 access_token=None,
59 refresh_token=None,
60 mac_key=None,
61 mac_algorithm=None,
62 token=None,
63 scope=None,
64 state=None,
65 redirect_url=None,
66 state_generator=generate_token,
67 code_verifier=None,
68 code_challenge=None,
69 code_challenge_method=None,
70 **kwargs):
71 """Initialize a client with commonly used attributes.
72
73 :param client_id: Client identifier given by the OAuth provider upon
74 registration.
75
76 :param default_token_placement: Tokens can be supplied in the Authorization
77 header (default), the URL query component (``query``) or the request
78 body (``body``).
79
80 :param token_type: OAuth 2 token type. Defaults to Bearer. Change this
81 if you specify the ``access_token`` parameter and know it is of a
82 different token type, such as a MAC, JWT or SAML token. Can
83 also be supplied as ``token_type`` inside the ``token`` dict parameter.
84
85 :param access_token: An access token (string) used to authenticate
86 requests to protected resources. Can also be supplied inside the
87 ``token`` dict parameter.
88
89 :param refresh_token: A refresh token (string) used to refresh expired
90 tokens. Can also be supplied inside the ``token`` dict parameter.
91
92 :param mac_key: Encryption key used with MAC tokens.
93
94 :param mac_algorithm: Hashing algorithm for MAC tokens.
95
96 :param token: A dict of token attributes such as ``access_token``,
97 ``token_type`` and ``expires_at``.
98
99 :param scope: A list of default scopes to request authorization for.
100
101 :param state: A CSRF protection string used during authorization.
102
103 :param redirect_url: The redirection endpoint on the client side to which
104 the user returns after authorization.
105
106 :param state_generator: A no argument state generation callable. Defaults
107 to :py:meth:`oauthlib.common.generate_token`.
108
109 :param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
110 authorization request to the token request.
111
112 :param code_challenge: PKCE parameter. A challenge derived from the code verifier that is sent in the
113 authorization request, to be verified against later.
114
115 :param code_challenge_method: PKCE parameter. A method that was used to derive code challenge.
116 Defaults to "plain" if not present in the request.
117 """
118
119 self.client_id = client_id
120 self.default_token_placement = default_token_placement
121 self.token_type = token_type
122 self.access_token = access_token
123 self.refresh_token = refresh_token
124 self.mac_key = mac_key
125 self.mac_algorithm = mac_algorithm
126 self.token = token or {}
127 self.scope = scope
128 self.state_generator = state_generator
129 self.state = state
130 self.redirect_url = redirect_url
131 self.code_verifier = code_verifier
132 self.code_challenge = code_challenge
133 self.code_challenge_method = code_challenge_method
134 self.code = None
135 self.expires_in = None
136 self._expires_at = None
137 self.populate_token_attributes(self.token)
138
139 @property
140 def token_types(self):
141 """Supported token types and their respective methods
142
143 Additional tokens can be supported by extending this dictionary.
144
145 The Bearer token spec is stable and safe to use.
146
147 The MAC token spec is not yet stable and support for MAC tokens
148 is experimental and currently matching version 00 of the spec.
149 """
150 return {
151 'Bearer': self._add_bearer_token,
152 'MAC': self._add_mac_token
153 }
154
155 def prepare_request_uri(self, *args, **kwargs):
156 """Abstract method used to create request URIs."""
157 raise NotImplementedError("Must be implemented by inheriting classes.")
158
159 def prepare_request_body(self, *args, **kwargs):
160 """Abstract method used to create request bodies."""
161 raise NotImplementedError("Must be implemented by inheriting classes.")
162
163 def parse_request_uri_response(self, *args, **kwargs):
164 """Abstract method used to parse redirection responses."""
165 raise NotImplementedError("Must be implemented by inheriting classes.")
166
167 def add_token(self, uri, http_method='GET', body=None, headers=None,
168 token_placement=None, **kwargs):
169 """Add token to the request uri, body or authorization header.
170
171 The access token type provides the client with the information
172 required to successfully utilize the access token to make a protected
173 resource request (along with type-specific attributes). The client
174 MUST NOT use an access token if it does not understand the token
175 type.
176
177 For example, the "bearer" token type defined in
178 [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
179 token string in the request:
180
181 .. code-block:: http
182
183 GET /resource/1 HTTP/1.1
184 Host: example.com
185 Authorization: Bearer mF_9.B5f-4.1JqM
186
187 while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
188 utilized by issuing a MAC key together with the access token which is
189 used to sign certain components of the HTTP requests:
190
191 .. code-block:: http
192
193 GET /resource/1 HTTP/1.1
194 Host: example.com
195 Authorization: MAC id="h480djs93hd8",
196 nonce="274312:dj83hs9s",
197 mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
198
199 .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2
200 .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2
201 """
202 if not is_secure_transport(uri):
203 raise InsecureTransportError()
204
205 token_placement = token_placement or self.default_token_placement
206
207 case_insensitive_token_types = {
208 k.lower(): v for k, v in self.token_types.items()}
209 if self.token_type.lower() not in case_insensitive_token_types:
210 raise ValueError("Unsupported token type: %s" % self.token_type)
211
212 if not (self.access_token or self.token.get('access_token')):
213 raise ValueError("Missing access token.")
214
215 if self._expires_at and self._expires_at < time.time():
216 raise TokenExpiredError()
217
218 return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
219 headers, token_placement, **kwargs)
220
221 def prepare_authorization_request(self, authorization_url, state=None,
222 redirect_url=None, scope=None, **kwargs):
223 """Prepare the authorization request.
224
225 This is the first step in many OAuth flows in which the user is
226 redirected to a certain authorization URL. This method adds
227 required parameters to the authorization URL.
228
229 :param authorization_url: Provider authorization endpoint URL.
230 :param state: CSRF protection string. Will be automatically created if
231 not provided. The generated state is available via the ``state``
232 attribute. Clients should verify that the state is unchanged and
233 present in the authorization response. This verification is done
234 automatically if using the ``authorization_response`` parameter
235 with ``prepare_token_request``.
236 :param redirect_url: Redirect URL to which the user will be returned
237 after authorization. Must be provided unless previously setup with
238 the provider. If provided then it must also be provided in the
239 token request.
240 :param scope: List of scopes to request. Must be equal to
241 or a subset of the scopes granted when obtaining the refresh
242 token. If none is provided, the ones provided in the constructor are
243 used.
244 :param kwargs: Additional parameters to included in the request.
245 :returns: The prepared request tuple with (url, headers, body).
246 """
247 if not is_secure_transport(authorization_url):
248 raise InsecureTransportError()
249
250 self.state = state or self.state_generator()
251 self.redirect_url = redirect_url or self.redirect_url
252 # do not assign scope to self automatically anymore
253 scope = self.scope if scope is None else scope
254 auth_url = self.prepare_request_uri(
255 authorization_url, redirect_uri=self.redirect_url,
256 scope=scope, state=self.state, **kwargs)
257 return auth_url, FORM_ENC_HEADERS, ''
258
259 def prepare_token_request(self, token_url, authorization_response=None,
260 redirect_url=None, state=None, body='', **kwargs):
261 """Prepare a token creation request.
262
263 Note that these requests usually require client authentication, either
264 by including client_id or a set of provider specific authentication
265 credentials.
266
267 :param token_url: Provider token creation endpoint URL.
268 :param authorization_response: The full redirection URL string, i.e.
269 the location to which the user was redirected after successful
270 authorization. Used to mine credentials needed to obtain a token
271 in this step, such as authorization code.
272 :param redirect_url: The redirect_url supplied with the authorization
273 request (if there was one).
274 :param state:
275 :param body: Existing request body (URL encoded string) to embed parameters
276 into. This may contain extra parameters. Default ''.
277 :param kwargs: Additional parameters to included in the request.
278 :returns: The prepared request tuple with (url, headers, body).
279 """
280 if not is_secure_transport(token_url):
281 raise InsecureTransportError()
282
283 state = state or self.state
284 if authorization_response:
285 self.parse_request_uri_response(
286 authorization_response, state=state)
287 self.redirect_url = redirect_url or self.redirect_url
288 body = self.prepare_request_body(body=body,
289 redirect_uri=self.redirect_url, **kwargs)
290
291 return token_url, FORM_ENC_HEADERS, body
292
293 def prepare_refresh_token_request(self, token_url, refresh_token=None,
294 body='', scope=None, **kwargs):
295 """Prepare an access token refresh request.
296
297 Expired access tokens can be replaced by new access tokens without
298 going through the OAuth dance if the client obtained a refresh token.
299 This refresh token and authentication credentials can be used to
300 obtain a new access token, and possibly a new refresh token.
301
302 :param token_url: Provider token refresh endpoint URL.
303 :param refresh_token: Refresh token string.
304 :param body: Existing request body (URL encoded string) to embed parameters
305 into. This may contain extra parameters. Default ''.
306 :param scope: List of scopes to request. Must be equal to
307 or a subset of the scopes granted when obtaining the refresh
308 token. If none is provided, the ones provided in the constructor are
309 used.
310 :param kwargs: Additional parameters to included in the request.
311 :returns: The prepared request tuple with (url, headers, body).
312 """
313 if not is_secure_transport(token_url):
314 raise InsecureTransportError()
315
316 # do not assign scope to self automatically anymore
317 scope = self.scope if scope is None else scope
318 body = self.prepare_refresh_body(body=body,
319 refresh_token=refresh_token, scope=scope, **kwargs)
320 return token_url, FORM_ENC_HEADERS, body
321
322 def prepare_token_revocation_request(self, revocation_url, token,
323 token_type_hint="access_token", body='', callback=None, **kwargs):
324 """Prepare a token revocation request.
325
326 :param revocation_url: Provider token revocation endpoint URL.
327 :param token: The access or refresh token to be revoked (string).
328 :param token_type_hint: ``"access_token"`` (default) or
329 ``"refresh_token"``. This is optional and if you wish to not pass it you
330 must provide ``token_type_hint=None``.
331 :param body:
332 :param callback: A jsonp callback such as ``package.callback`` to be invoked
333 upon receiving the response. Not that it should not include a () suffix.
334 :param kwargs: Additional parameters to included in the request.
335 :returns: The prepared request tuple with (url, headers, body).
336
337 Note that JSONP request may use GET requests as the parameters will
338 be added to the request URL query as opposed to the request body.
339
340 An example of a revocation request
341
342 .. code-block:: http
343
344 POST /revoke HTTP/1.1
345 Host: server.example.com
346 Content-Type: application/x-www-form-urlencoded
347 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
348
349 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
350
351 An example of a jsonp revocation request
352
353 .. code-block:: http
354
355 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
356 Host: server.example.com
357 Content-Type: application/x-www-form-urlencoded
358 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
359
360 and an error response
361
362 .. code-block:: javascript
363
364 package.myCallback({"error":"unsupported_token_type"});
365
366 Note that these requests usually require client credentials, client_id in
367 the case for public clients and provider specific authentication
368 credentials for confidential clients.
369 """
370 if not is_secure_transport(revocation_url):
371 raise InsecureTransportError()
372
373 return prepare_token_revocation_request(revocation_url, token,
374 token_type_hint=token_type_hint, body=body, callback=callback,
375 **kwargs)
376
377 def parse_request_body_response(self, body, scope=None, **kwargs):
378 """Parse the JSON response body.
379
380 If the access token request is valid and authorized, the
381 authorization server issues an access token as described in
382 `Section 5.1`_. A refresh token SHOULD NOT be included. If the request
383 failed client authentication or is invalid, the authorization server
384 returns an error response as described in `Section 5.2`_.
385
386 :param body: The response body from the token request.
387 :param scope: Scopes originally requested. If none is provided, the ones
388 provided in the constructor are used.
389 :return: Dictionary of token parameters.
390 :raises: Warning if scope has changed. :py:class:`oauthlib.oauth2.errors.OAuth2Error`
391 if response is invalid.
392
393 These response are json encoded and could easily be parsed without
394 the assistance of OAuthLib. However, there are a few subtle issues
395 to be aware of regarding the response which are helpfully addressed
396 through the raising of various errors.
397
398 A successful response should always contain
399
400 **access_token**
401 The access token issued by the authorization server. Often
402 a random string.
403
404 **token_type**
405 The type of the token issued as described in `Section 7.1`_.
406 Commonly ``Bearer``.
407
408 While it is not mandated it is recommended that the provider include
409
410 **expires_in**
411 The lifetime in seconds of the access token. For
412 example, the value "3600" denotes that the access token will
413 expire in one hour from the time the response was generated.
414 If omitted, the authorization server SHOULD provide the
415 expiration time via other means or document the default value.
416
417 **scope**
418 Providers may supply this in all responses but are required to only
419 if it has changed since the authorization request.
420
421 .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
422 .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
423 .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
424 """
425 scope = self.scope if scope is None else scope
426 self.token = parse_token_response(body, scope=scope)
427 self.populate_token_attributes(self.token)
428 return self.token
429
430 def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
431 """Prepare an access token request, using a refresh token.
432
433 If the authorization server issued a refresh token to the client, the
434 client makes a refresh request to the token endpoint by adding the
435 following parameters using the `application/x-www-form-urlencoded`
436 format in the HTTP request entity-body:
437
438 :param refresh_token: REQUIRED. The refresh token issued to the client.
439 :param scope: OPTIONAL. The scope of the access request as described by
440 Section 3.3. The requested scope MUST NOT include any scope
441 not originally granted by the resource owner, and if omitted is
442 treated as equal to the scope originally granted by the
443 resource owner. Note that if none is provided, the ones provided
444 in the constructor are used if any.
445 """
446 refresh_token = refresh_token or self.refresh_token
447 scope = self.scope if scope is None else scope
448 return prepare_token_request(self.refresh_token_key, body=body, scope=scope,
449 refresh_token=refresh_token, **kwargs)
450
451 def _add_bearer_token(self, uri, http_method='GET', body=None,
452 headers=None, token_placement=None):
453 """Add a bearer token to the request uri, body or authorization header."""
454 if token_placement == AUTH_HEADER:
455 headers = tokens.prepare_bearer_headers(self.access_token, headers)
456
457 elif token_placement == URI_QUERY:
458 uri = tokens.prepare_bearer_uri(self.access_token, uri)
459
460 elif token_placement == BODY:
461 body = tokens.prepare_bearer_body(self.access_token, body)
462
463 else:
464 raise ValueError("Invalid token placement.")
465 return uri, headers, body
466
467 def create_code_verifier(self, length):
468 """Create PKCE **code_verifier** used in computing **code_challenge**.
469 See `RFC7636 Section 4.1`_
470
471 :param length: REQUIRED. The length of the code_verifier.
472
473 The client first creates a code verifier, "code_verifier", for each
474 OAuth 2.0 [RFC6749] Authorization Request, in the following manner:
475
476 .. code-block:: text
477
478 code_verifier = high-entropy cryptographic random STRING using the
479 unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
480 from Section 2.3 of [RFC3986], with a minimum length of 43 characters
481 and a maximum length of 128 characters.
482
483 .. _`RFC7636 Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1
484 """
485 code_verifier = None
486
487 if not length >= 43:
488 raise ValueError("Length must be greater than or equal to 43")
489
490 if not length <= 128:
491 raise ValueError("Length must be less than or equal to 128")
492
493 code_verifier = generate_token(length, UNICODE_ASCII_CHARACTER_SET + "-._~")
494
495 self.code_verifier = code_verifier
496
497 return code_verifier
498
499 def create_code_challenge(self, code_verifier, code_challenge_method=None):
500 """Create PKCE **code_challenge** derived from the **code_verifier**.
501 See `RFC7636 Section 4.2`_
502
503 :param code_verifier: REQUIRED. The **code_verifier** generated from `create_code_verifier()`.
504 :param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable values include `S256`. DEFAULT is `plain`.
505
506 The client then creates a code challenge derived from the code
507 verifier by using one of the following transformations on the code
508 verifier::
509
510 plain
511 code_challenge = code_verifier
512 S256
513 code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
514
515 If the client is capable of using `S256`, it MUST use `S256`, as
516 `S256` is Mandatory To Implement (MTI) on the server. Clients are
517 permitted to use `plain` only if they cannot support `S256` for some
518 technical reason and know via out-of-band configuration that the
519 server supports `plain`.
520
521 The plain transformation is for compatibility with existing
522 deployments and for constrained environments that can't use the S256 transformation.
523
524 .. _`RFC7636 Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2
525 """
526 code_challenge = None
527
528 if code_verifier is None:
529 raise ValueError("Invalid code_verifier")
530
531 if code_challenge_method is None:
532 code_challenge_method = "plain"
533 self.code_challenge_method = code_challenge_method
534 code_challenge = code_verifier
535 self.code_challenge = code_challenge
536
537 if code_challenge_method == "S256":
538 h = hashlib.sha256()
539 h.update(code_verifier.encode(encoding='ascii'))
540 sha256_val = h.digest()
541 code_challenge = bytes.decode(base64.urlsafe_b64encode(sha256_val))
542 # replace '+' with '-', '/' with '_', and remove trailing '='
543 code_challenge = code_challenge.replace("+", "-").replace("/", "_").replace("=", "")
544 self.code_challenge = code_challenge
545
546 return code_challenge
547
548 def _add_mac_token(self, uri, http_method='GET', body=None,
549 headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
550 """Add a MAC token to the request authorization header.
551
552 Warning: MAC token support is experimental as the spec is not yet stable.
553 """
554 if token_placement != AUTH_HEADER:
555 raise ValueError("Invalid token placement.")
556
557 headers = tokens.prepare_mac_header(self.access_token, uri,
558 self.mac_key, http_method, headers=headers, body=body, ext=ext,
559 hash_algorithm=self.mac_algorithm, **kwargs)
560 return uri, headers, body
561
562 def _populate_attributes(self, response):
563 warnings.warn("Please switch to the public method "
564 "populate_token_attributes.", DeprecationWarning)
565 return self.populate_token_attributes(response)
566
567 def populate_code_attributes(self, response):
568 """Add attributes from an auth code response to self."""
569
570 if 'code' in response:
571 self.code = response.get('code')
572
573 def populate_token_attributes(self, response):
574 """Add attributes from a token exchange response to self."""
575
576 if 'access_token' in response:
577 self.access_token = response.get('access_token')
578
579 if 'refresh_token' in response:
580 self.refresh_token = response.get('refresh_token')
581
582 if 'token_type' in response:
583 self.token_type = response.get('token_type')
584
585 vin, vat, v_at = parse_expires(response)
586 if vin:
587 self.expires_in = vin
588 if vat:
589 self.expires_at = vat
590 if v_at:
591 self._expires_at = v_at
592
593 if 'mac_key' in response:
594 self.mac_key = response.get('mac_key')
595
596 if 'mac_algorithm' in response:
597 self.mac_algorithm = response.get('mac_algorithm')