summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Huot <JonathanHuot@users.noreply.github.com>2018-09-21 23:40:59 +0200
committerGitHub <noreply@github.com>2018-09-21 23:40:59 +0200
commitfabcf865a7f33e5592453189670a82f08dc4384b (patch)
treef0750c8ccb5239d13b42e3a38302c7ed52b43098
parent326456cb78eb6b50e6f44f01cb0eaccc7652cf1f (diff)
parent127a3b58df22798bad9bb2e30743c2401c978d8a (diff)
downloadoauthlib-fabcf865a7f33e5592453189670a82f08dc4384b.tar.gz
Merge pull request #593 from jvanasco/fix-585_client_id
PR for #585, `client_id` behavior with `prepare_request_body`
-rw-r--r--CHANGELOG.rst10
-rw-r--r--oauthlib/oauth2/rfc6749/clients/backend_application.py15
-rw-r--r--oauthlib/oauth2/rfc6749/clients/base.py6
-rw-r--r--oauthlib/oauth2/rfc6749/clients/legacy_application.py13
-rw-r--r--oauthlib/oauth2/rfc6749/clients/service_application.py31
-rw-r--r--oauthlib/oauth2/rfc6749/clients/web_application.py39
-rw-r--r--oauthlib/oauth2/rfc6749/parameters.py49
-rw-r--r--tests/oauth2/rfc6749/clients/test_backend_application.py1
-rw-r--r--tests/oauth2/rfc6749/clients/test_legacy_application.py62
-rw-r--r--tests/oauth2/rfc6749/clients/test_web_application.py82
10 files changed, 281 insertions, 27 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a8e1941..fd53769 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,16 @@
Changelog
=========
+Unreleased
+------------------
+
+* OAuth2's `prepare_token_request` supports sending an empty string for `client_id` (#585)
+* OAuth2's `WebApplicationClient.prepare_request_body` was refactored to better
+ support sending or omitting the `client_id` via a new `include_client_id` kwarg.
+ By default this is included. The method will also emit a DeprecationWarning if
+ a `client_id` parameter is submitted; the already configured `self.client_id`
+ is the preferred option. (#585)
+
2.1.0 (2018-05-21)
------------------
diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py
index cbad8b7..cd46f12 100644
--- a/oauthlib/oauth2/rfc6749/clients/backend_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py
@@ -30,15 +30,26 @@ class BackendApplicationClient(Client):
no additional authorization request is needed.
"""
- def prepare_request_body(self, body='', scope=None, **kwargs):
+ def prepare_request_body(self, body='', scope=None,
+ include_client_id=None, **kwargs):
"""Add the client credentials to the request body.
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format per `Appendix B`_ in the HTTP request entity-body:
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
:param scope: The scope of the access request as described by
`Section 3.3`_.
+
+ :param include_client_id: `True` to send the `client_id` in the body of
+ the upstream request. Default `None`. This is
+ required if the client is not authenticating
+ with the authorization server as described
+ in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
:param kwargs: Extra credentials to include in the token request.
The client MUST authenticate with the authorization server as
@@ -56,5 +67,7 @@ class BackendApplicationClient(Client):
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
return prepare_token_request('client_credentials', body=body,
scope=scope, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py
index 406832d..d8ded50 100644
--- a/oauthlib/oauth2/rfc6749/clients/base.py
+++ b/oauthlib/oauth2/rfc6749/clients/base.py
@@ -254,7 +254,8 @@ class Client(object):
:param redirect_url: The redirect_url supplied with the authorization
request (if there was one).
- :param body: Request body (URL encoded string).
+ :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.
@@ -286,7 +287,8 @@ class Client(object):
:param refresh_token: Refresh token string.
- :param body: Request body (URL encoded 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
diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
index b16fc9f..a13927a 100644
--- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
@@ -38,7 +38,8 @@ class LegacyApplicationClient(Client):
def __init__(self, client_id, **kwargs):
super(LegacyApplicationClient, self).__init__(client_id, **kwargs)
- def prepare_request_body(self, username, password, body='', scope=None, **kwargs):
+ def prepare_request_body(self, username, password, body='', scope=None,
+ include_client_id=None, **kwargs):
"""Add the resource owner password and username to the request body.
The client makes a request to the token endpoint by adding the
@@ -47,8 +48,16 @@ class LegacyApplicationClient(Client):
:param username: The resource owner username.
:param password: The resource owner password.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
:param scope: The scope of the access request as described by
`Section 3.3`_.
+ :param include_client_id: `True` to send the `client_id` in the body of
+ the upstream request. Default `None`. This is
+ required if the client is not authenticating
+ with the authorization server as described
+ in `Section 3.2.1`_.
+ :type include_client_id: Boolean
:param kwargs: Extra credentials to include in the token request.
If the client type is confidential or the client was issued client
@@ -68,5 +77,7 @@ class LegacyApplicationClient(Client):
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
return prepare_token_request('password', body=body, username=username,
password=password, scope=scope, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py
index 3045676..35333d8 100644
--- a/oauthlib/oauth2/rfc6749/clients/service_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/service_application.py
@@ -72,7 +72,8 @@ class ServiceApplicationClient(Client):
issued_at=None,
extra_claims=None,
body='',
- scope=None,
+ scope=None,
+ include_client_id=None,
**kwargs):
"""Create and add a JWT assertion to the request body.
@@ -97,20 +98,32 @@ class ServiceApplicationClient(Client):
:param issued_at: A unix timestamp of when the JWT was created.
Defaults to now, i.e. ``time.time()``.
- :param not_before: A unix timestamp after which the JWT may be used.
- Not included unless provided.
-
- :param jwt_id: A unique JWT token identifier. Not included unless
- provided.
-
:param extra_claims: A dict of additional claims to include in the JWT.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+
:param scope: The scope of the access request.
- :param body: Request body (string) with extra parameters.
+ :param include_client_id: `True` to send the `client_id` in the body of
+ the upstream request. Default `None`. This is
+ required if the client is not authenticating
+ with the authorization server as described
+ in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
+ :param not_before: A unix timestamp after which the JWT may be used.
+ Not included unless provided. *
+
+ :param jwt_id: A unique JWT token identifier. Not included unless
+ provided. *
:param kwargs: Extra credentials to include in the token request.
+ Parameters marked with a `*` above are not explicit arguments in the
+ function signature, but are specially documented arguments for items
+ appearing in the generic `**kwargs` keyworded input.
+
The "scope" parameter may be used, as defined in the Assertion
Framework for OAuth 2.0 Client Authentication and Authorization Grants
[I-D.ietf-oauth-assertions] specification, to indicate the requested
@@ -168,6 +181,8 @@ class ServiceApplicationClient(Client):
assertion = jwt.encode(claim, key, 'RS256')
assertion = to_unicode(assertion)
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
return prepare_token_request(self.grant_type,
body=body,
assertion=assertion,
diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py
index c14a5f8..487e3a0 100644
--- a/oauthlib/oauth2/rfc6749/clients/web_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/web_application.py
@@ -8,6 +8,8 @@ for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
+import warnings
+
from ..parameters import (parse_authorization_code_response,
parse_token_response, prepare_grant_uri,
prepare_token_request)
@@ -85,17 +87,14 @@ class WebApplicationClient(Client):
return prepare_grant_uri(uri, self.client_id, 'code',
redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
- def prepare_request_body(self, client_id=None, code=None, body='',
- redirect_uri=None, **kwargs):
+ def prepare_request_body(self, code=None, redirect_uri=None, body='',
+ include_client_id=True, **kwargs):
"""Prepare the access token request body.
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format in the HTTP request entity-body:
- :param client_id: REQUIRED, if the client is not authenticating with the
- authorization server as described in `Section 3.2.1`_.
-
:param code: REQUIRED. The authorization code received from the
authorization server.
@@ -103,6 +102,15 @@ class WebApplicationClient(Client):
authorization request as described in `Section 4.1.1`_, and their
values MUST be identical.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+
+ :param include_client_id: `True` (default) to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
:param kwargs: Extra parameters to include in the token request.
In addition OAuthLib will add the ``grant_type`` parameter set to
@@ -120,12 +128,31 @@ class WebApplicationClient(Client):
>>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
+ `Section 3.2.1` also states:
+ In the "authorization_code" "grant_type" request to the token
+ endpoint, an unauthenticated client MUST send its "client_id" to
+ prevent itself from inadvertently accepting a code intended for a
+ client with a different "client_id". This protects the client from
+ substitution of the authentication code. (It provides no additional
+ security for the protected resource.)
+
.. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
code = code or self.code
+ if 'client_id' in kwargs:
+ warnings.warn("`client_id` has been deprecated in favor of "
+ "`include_client_id`, a boolean value which will "
+ "include the already configured `self.client_id`.",
+ DeprecationWarning)
+ if kwargs['client_id'] != self.client_id:
+ raise ValueError("`client_id` was supplied as an argument, but "
+ "it does not match `self.client_id`")
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
return prepare_token_request('authorization_code', code=code, body=body,
- client_id=client_id, redirect_uri=redirect_uri, **kwargs)
+ redirect_uri=redirect_uri, **kwargs)
def parse_request_uri_response(self, uri, state=None):
"""Parse the URI query for code and state.
diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py
index 74a15f9..4d0baee 100644
--- a/oauthlib/oauth2/rfc6749/parameters.py
+++ b/oauthlib/oauth2/rfc6749/parameters.py
@@ -87,7 +87,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
return add_params_to_uri(uri, params)
-def prepare_token_request(grant_type, body='', **kwargs):
+def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs):
"""Prepare the access token request.
The client makes a request to the token endpoint by adding the
@@ -96,14 +96,38 @@ def prepare_token_request(grant_type, body='', **kwargs):
:param grant_type: To indicate grant type being used, i.e. "password",
"authorization_code" or "client_credentials".
- :param body: Existing request body to embed parameters in.
- :param code: If using authorization code grant, pass the previously
- obtained authorization code as the ``code`` argument.
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+
+ :param include_client_id: `True` (default) to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
+ :param client_id: Unicode client identifier. Will only appear if
+ `include_client_id` is True. *
+
+ :param client_secret: Unicode client secret. Will only appear if set to a
+ value that is not `None`. Invoking this function with
+ an empty string will send an empty `client_secret`
+ value to the server. *
+
+ :param code: If using authorization_code grant, pass the previously
+ obtained authorization code as the ``code`` argument. *
+
:param redirect_uri: If the "redirect_uri" parameter was included in the
authorization request as described in
- `Section 4.1.1`_, and their values MUST be identical.
+ `Section 4.1.1`_, and their values MUST be identical. *
+
:param kwargs: Extra arguments to embed in the request body.
+ Parameters marked with a `*` above are not explicit arguments in the
+ function signature, but are specially documented arguments for items
+ appearing in the generic `**kwargs` keyworded input.
+
An example of an authorization code token request body:
.. code-block:: http
@@ -118,6 +142,19 @@ def prepare_token_request(grant_type, body='', **kwargs):
if 'scope' in kwargs:
kwargs['scope'] = list_to_scope(kwargs['scope'])
+ # pull the `client_id` out of the kwargs.
+ client_id = kwargs.pop('client_id', None)
+ if include_client_id:
+ if client_id is not None:
+ params.append((unicode_type('client_id'), client_id))
+
+ # the kwargs iteration below only supports including boolean truth (truthy)
+ # values, but some servers may require an empty string for `client_secret`
+ client_secret = kwargs.pop('client_secret', None)
+ if client_secret is not None:
+ params.append((unicode_type('client_secret'), client_secret))
+
+ # this handles: `code`, `redirect_uri`, and other undocumented params
for k in kwargs:
if kwargs[k]:
params.append((unicode_type(k), kwargs[k]))
@@ -144,7 +181,7 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token",
token types. An authorization server MAY ignore
this parameter, particularly if it is able to detect
the token type automatically.
-
+
This specification defines two values for `token_type_hint`:
* access_token: An access token as defined in [RFC6749],
diff --git a/tests/oauth2/rfc6749/clients/test_backend_application.py b/tests/oauth2/rfc6749/clients/test_backend_application.py
index 6b342f0..aa2ba2b 100644
--- a/tests/oauth2/rfc6749/clients/test_backend_application.py
+++ b/tests/oauth2/rfc6749/clients/test_backend_application.py
@@ -15,6 +15,7 @@ from ....unittest import TestCase
class BackendApplicationClientTest(TestCase):
client_id = "someclientid"
+ client_secret = 'someclientsecret'
scope = ["/profile"]
kwargs = {
"some": "providers",
diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py
index 3f97c02..21af4a3 100644
--- a/tests/oauth2/rfc6749/clients/test_legacy_application.py
+++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py
@@ -10,19 +10,26 @@ from oauthlib.oauth2 import LegacyApplicationClient
from ....unittest import TestCase
+# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
@patch('time.time', new=lambda: 1000)
class LegacyApplicationClientTest(TestCase):
client_id = "someclientid"
+ client_secret = 'someclientsecret'
scope = ["/profile"]
kwargs = {
"some": "providers",
"require": "extra arguments"
}
- username = "foo"
- password = "bar"
+ username = "user_username"
+ password = "user_password"
body = "not=empty"
body_up = "not=empty&grant_type=password&username=%s&password=%s" % (username, password)
@@ -88,3 +95,54 @@ class LegacyApplicationClientTest(TestCase):
finally:
signals.scope_changed.disconnect(record_scope_change)
del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
+
+ def test_prepare_request_body(self):
+ """
+ see issue #585
+ https://github.com/oauthlib/oauthlib/issues/585
+ """
+ client = LegacyApplicationClient(self.client_id)
+
+ # scenario 1, default behavior to not include `client_id`
+ r1 = client.prepare_request_body(username=self.username, password=self.password)
+ self.assertIn(r1, ('grant_type=password&username=%s&password=%s' % (self.username, self.password, ),
+ 'grant_type=password&password=%s&username=%s' % (self.password, self.username, ),
+ ))
+
+ # scenario 2, include `client_id` in the body
+ r2 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True)
+ r2_params = dict(urlparse.parse_qsl(r2, keep_blank_values=True))
+ self.assertEqual(len(r2_params.keys()), 4)
+ self.assertEqual(r2_params['grant_type'], 'password')
+ self.assertEqual(r2_params['username'], self.username)
+ self.assertEqual(r2_params['password'], self.password)
+ self.assertEqual(r2_params['client_id'], self.client_id)
+
+ # scenario 3, include `client_id` + `client_secret` in the body
+ r3 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=self.client_secret)
+ r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True))
+ self.assertEqual(len(r3_params.keys()), 5)
+ self.assertEqual(r3_params['grant_type'], 'password')
+ self.assertEqual(r3_params['username'], self.username)
+ self.assertEqual(r3_params['password'], self.password)
+ self.assertEqual(r3_params['client_id'], self.client_id)
+ self.assertEqual(r3_params['client_secret'], self.client_secret)
+
+ # scenario 4, `client_secret` is an empty string
+ r4 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret='')
+ r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True))
+ self.assertEqual(len(r4_params.keys()), 5)
+ self.assertEqual(r4_params['grant_type'], 'password')
+ self.assertEqual(r4_params['username'], self.username)
+ self.assertEqual(r4_params['password'], self.password)
+ self.assertEqual(r4_params['client_id'], self.client_id)
+ self.assertEqual(r4_params['client_secret'], '')
+
+ # scenario 4b`,` client_secret is `None`
+ r4b = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=None)
+ r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True))
+ self.assertEqual(len(r4b_params.keys()), 4)
+ self.assertEqual(r4b_params['grant_type'], 'password')
+ self.assertEqual(r4b_params['username'], self.username)
+ self.assertEqual(r4b_params['password'], self.password)
+ self.assertEqual(r4b_params['client_id'], self.client_id)
diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py
index 4ecc3b3..092f93e 100644
--- a/tests/oauth2/rfc6749/clients/test_web_application.py
+++ b/tests/oauth2/rfc6749/clients/test_web_application.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
import datetime
import os
+import warnings
from mock import patch
@@ -15,11 +16,18 @@ from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY
from ....unittest import TestCase
+# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
@patch('time.time', new=lambda: 1000)
class WebApplicationClientTest(TestCase):
client_id = "someclientid"
+ client_secret = 'someclientsecret'
uri = "https://example.com/path?query=world"
uri_id = uri + "&response_type=code&client_id=" + client_id
uri_redirect = uri_id + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback"
@@ -38,7 +46,7 @@ class WebApplicationClientTest(TestCase):
code = "zzzzaaaa"
body = "not=empty"
- body_code = "not=empty&grant_type=authorization_code&code=%s" % code
+ body_code = "not=empty&grant_type=authorization_code&code=%s&client_id=%s" % (code, client_id)
body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback"
body_kwargs = body_code + "&some=providers&require=extra+arguments"
@@ -177,3 +185,75 @@ class WebApplicationClientTest(TestCase):
# verify default header and body only
self.assertEqual(header, {'Content-Type': 'application/x-www-form-urlencoded'})
self.assertEqual(body, '')
+
+ def test_prepare_request_body(self):
+ """
+ see issue #585
+ https://github.com/oauthlib/oauthlib/issues/585
+
+ `prepare_request_body` should support the following scenarios:
+ 1. Include client_id alone in the body (default)
+ 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution)
+ 3. Include client_id and client_secret in the body (RFC alternative solution)
+ 4. Include client_id in the body and an empty string for client_secret.
+ """
+ client = WebApplicationClient(self.client_id)
+
+ # scenario 1, default behavior to include `client_id`
+ r1 = client.prepare_request_body()
+ self.assertEqual(r1, 'grant_type=authorization_code&client_id=%s' % self.client_id)
+
+ r1b = client.prepare_request_body(include_client_id=True)
+ self.assertEqual(r1b, 'grant_type=authorization_code&client_id=%s' % self.client_id)
+
+ # scenario 2, do not include `client_id` in the body, so it can be sent in auth.
+ r2 = client.prepare_request_body(include_client_id=False)
+ self.assertEqual(r2, 'grant_type=authorization_code')
+
+ # scenario 3, Include client_id and client_secret in the body (RFC alternative solution)
+ # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting
+ r3 = client.prepare_request_body(client_secret=self.client_secret)
+ r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True))
+ self.assertEqual(len(r3_params.keys()), 3)
+ self.assertEqual(r3_params['grant_type'], 'authorization_code')
+ self.assertEqual(r3_params['client_id'], self.client_id)
+ self.assertEqual(r3_params['client_secret'], self.client_secret)
+
+ r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret)
+ r3b_params = dict(urlparse.parse_qsl(r3b, keep_blank_values=True))
+ self.assertEqual(len(r3b_params.keys()), 3)
+ self.assertEqual(r3b_params['grant_type'], 'authorization_code')
+ self.assertEqual(r3b_params['client_id'], self.client_id)
+ self.assertEqual(r3b_params['client_secret'], self.client_secret)
+
+ # scenario 4, `client_secret` is an empty string
+ r4 = client.prepare_request_body(include_client_id=True, client_secret='')
+ r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True))
+ self.assertEqual(len(r4_params.keys()), 3)
+ self.assertEqual(r4_params['grant_type'], 'authorization_code')
+ self.assertEqual(r4_params['client_id'], self.client_id)
+ self.assertEqual(r4_params['client_secret'], '')
+
+ # scenario 4b, `client_secret` is `None`
+ r4b = client.prepare_request_body(include_client_id=True, client_secret=None)
+ r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True))
+ self.assertEqual(len(r4b_params.keys()), 2)
+ self.assertEqual(r4b_params['grant_type'], 'authorization_code')
+ self.assertEqual(r4b_params['client_id'], self.client_id)
+
+ # scenario Warnings
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always") # catch all
+
+ # warning1 - raise a DeprecationWarning if a `client_id` is submitted
+ rWarnings1 = client.prepare_request_body(client_id=self.client_id)
+ self.assertEqual(len(w), 1)
+ self.assertIsInstance(w[0].message, DeprecationWarning)
+
+ # testing the exact warning message in Python2&Python3 is a pain
+
+ # scenario Exceptions
+ # exception1 - raise a ValueError if the a different `client_id` is submitted
+ with self.assertRaises(ValueError) as cm:
+ client.prepare_request_body(client_id='different_client_id')
+ # testing the exact exception message in Python2&Python3 is a pain