summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml17
-rw-r--r--AUTHORS19
-rw-r--r--CHANGELOG.rst27
-rw-r--r--docs/feature_matrix.rst21
-rw-r--r--docs/oauth2/clients/deviceclient.rst5
-rw-r--r--oauthlib/__init__.py2
-rw-r--r--oauthlib/oauth2/__init__.py1
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/authorization_code.py18
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/base.py18
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/refresh_token.py1
-rw-r--r--oauthlib/oauth2/rfc6749/request_validator.py1
-rw-r--r--oauthlib/oauth2/rfc6749/tokens.py1
-rw-r--r--oauthlib/oauth2/rfc8628/__init__.py10
-rw-r--r--oauthlib/oauth2/rfc8628/clients/__init__.py8
-rw-r--r--oauthlib/oauth2/rfc8628/clients/device.py94
-rw-r--r--tests/oauth2/rfc6749/grant_types/test_refresh_token.py41
-rw-r--r--tests/oauth2/rfc8628/__init__.py0
-rw-r--r--tests/oauth2/rfc8628/clients/__init__.py0
-rw-r--r--tests/oauth2/rfc8628/clients/test_device.py63
-rw-r--r--tox.ini6
20 files changed, 310 insertions, 43 deletions
diff --git a/.travis.yml b/.travis.yml
index 89af15d..b2dad7a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,21 +1,26 @@
language: python
python: 3.8
+os: linux
dist: bionic
cache: pip
-matrix:
+jobs:
include:
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
- env: TOXENV=py37
+ env: TOXENV=py37,docs
- python: "3.8"
- env: TOXENV=py38,bandit,docs,readme
+ env: TOXENV=py38,bandit,readme
- python: "3.9"
env: TOXENV=py39
- - python: "3.10-dev"
+ - python: "3.10.2"
env: TOXENV=py310
+ - python: "3.11-dev"
+ env: TOXENV=py311
- python: "pypy3"
env: TOXENV=pypy3
+ allow_failures:
+ - python: "3.11-dev"
before_install:
- python -m pip install --upgrade pip setuptools
- python -m pip install tox coveralls
@@ -32,7 +37,7 @@ notifications:
on_start: never
deploy:
- provider: releases
- api_key:
+ token:
secure: "eqEWOzKWZCuvd1a77CA03OX/HCrsYlsu1/Sz/RhXQIEhKz6tKp10KGw9zr57bHAIl0OfJFK9k63lI2HOctAmwkKeeQ4HdNqw4pHFa8Gk3liGp31KSmshVtHX8Rtn0DuFA028Wm7w5n+fOVc8tJVU/UsKjsfsAzRHnQjMamckoXU="
skip_cleanup: true
on:
@@ -41,7 +46,7 @@ deploy:
condition: $TOXENV = py36
repo: oauthlib/oauthlib
- provider: pypi
- user: JonathanHuot
+ username: JonathanHuot
password:
secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ="
distributions: sdist bdist_wheel
diff --git a/AUTHORS b/AUTHORS
index bbffe14..c820d6d 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -30,3 +30,22 @@ Jonathan Huot
Pieter Ennes
Olaf Conradi
Tom Evans
+Bella Woo
+Alan Crosswell
+Nikos Sklikas
+Paul Dekkers
+Jason com4
+Aman Singh Solanki
+uy-rrodriguez
+Sylvain MariƩ
+Hoylen Sue
+Christian Clauss
+Mike Kelly
+Xpyder
+Theron Luhn
+Alexander Freeman1981
+Jon Velando
+Scott Gifford
+Hugo van Kemenade
+Richard Connon
+Karim Kanso
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 21c9159..d7882e9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,15 +1,34 @@
Changelog
=========
+3.2.0 (2022-01-29)
+------------------
+OAuth2.0 Client:
+* #795: Add Device Authorization Flow for Web Application
+* #786: Add PKCE support for Client
+* #783: Fallback to none in case of wrong expires_at format.
+
+OAuth2.0 Provider:
+* #790: Add support for CORS to metadata endpoint.
+* #791: Add support for CORS to token endpoint.
+* #787: Remove comma after Bearer in WWW-Authenticate
+
+OAuth2.0 Provider - OIDC:
+ * #755: Call save_token in Hybrid code flow
+ * #751: OIDC add support of refreshing ID Tokens with `refresh_id_token`
+ * #751: The RefreshTokenGrant modifiers now take the same arguments as the
+ AuthorizationCodeGrant modifiers (`token`, `token_handler`, `request`).
+
+General:
+ * Added Python 3.9, 3.10, 3.11
+ * Improve Travis & Coverage
+
3.1.1 (2021-05-31)
------------------
OAuth2.0 Provider - Bugfixes
* #753: Fix acceptance of valid IPv6 addresses in URI validation
-OAuth2.0 Provider - Features
- * #751: OIDC add support of refreshing ID Tokens
-
OAuth2.0 Client - Bugfixes
* #730: Base OAuth2 Client now has a consistent way of managing the `scope`: it consistently
@@ -28,8 +47,6 @@ OAuth2.0 Provider - Bugfixes
* #746: OpenID Connect Hybrid - fix nonce not passed to add_id_token
* #756: Different prompt values are now handled according to spec (e.g. prompt=none)
* #759: OpenID Connect - fix Authorization: Basic parsing
- * #751: The RefreshTokenGrant modifiers now take the same arguments as the
- AuthorizationCodeGrant modifiers (`token`, `token_handler`, `request`).
General
* #716: improved skeleton validator for public vs private client
diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst
index 56d0cf3..f9309f9 100644
--- a/docs/feature_matrix.rst
+++ b/docs/feature_matrix.rst
@@ -1,8 +1,8 @@
-Supported features and platforms
-================================
+Features and platforms
+======================
-Features
---------
+.. contents::
+ :local:
OAuth 1.0a
..........
@@ -39,16 +39,16 @@ OAuth 2.0 client and provider support for:
- `RFC 6749 section-6`_: Refresh Tokens
- `RFC 6750`_: Bearer Tokens
- `RFC 7009`_: Token Revocation
+- `RFC 7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
+- `RFC 8628`_: OAuth2.0 Device Authorization Grant
- `RFC Draft`_ Message Authentication Code (MAC) Tokens
+
+Partial implementations (any help/PR are welcomed to complete the list):
+
- OAuth2.0 Provider: `OpenID Connect Core`_
-- OAuth2.0 Provider: `RFC 7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
- OAuth2.0 Provider: `RFC 7662`_: Token Introspection
- OAuth2.0 Provider: `RFC 8414`_: Authorization Server Metadata
-
-Features to be implemented (any help/PR are welcomed):
-
- OAuth2.0 **Client**: `OpenID Connect Core`_
-- OAuth2.0 **Client**: `RFC 7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
- OAuth2.0 **Client**: `RFC 7662`_: Token Introspection
- OAuth2.0 **Client**: `RFC 8414`_: Authorization Server Metadata
- SAML2
@@ -59,7 +59,7 @@ Features to be implemented (any help/PR are welcomed):
- ...and more
Platforms
----------
+.........
OAuthLib is mainly developed and tested on 64-bit Linux. It works on
Unix and Unix-like operating systems (including macOS), as well as
@@ -85,5 +85,6 @@ additional packages: see the installation instructions for details.
.. _`RFC 7009`: https://tools.ietf.org/html/rfc7009
.. _`RFC 7662`: https://tools.ietf.org/html/rfc7662
.. _`RFC 7636`: https://tools.ietf.org/html/rfc7636
+.. _`RFC 8628`: https://tools.ietf.org/html/rfc8628
.. _`OpenID Connect Core`: https://openid.net/specs/openid-connect-core-1_0.html
.. _`RFC 8414`: https://tools.ietf.org/html/rfc8414
diff --git a/docs/oauth2/clients/deviceclient.rst b/docs/oauth2/clients/deviceclient.rst
new file mode 100644
index 0000000..d4e8d7d
--- /dev/null
+++ b/docs/oauth2/clients/deviceclient.rst
@@ -0,0 +1,5 @@
+DeviceClient
+------------------------
+
+.. autoclass:: oauthlib.oauth2.DeviceClient
+ :members:
diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py
index a94cf94..5dbffc9 100644
--- a/oauthlib/__init__.py
+++ b/oauthlib/__init__.py
@@ -12,7 +12,7 @@ import logging
from logging import NullHandler
__author__ = 'The OAuthlib Community'
-__version__ = '3.1.1'
+__version__ = '3.2.0'
logging.getLogger('oauthlib').addHandler(NullHandler())
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py
index a6e1ccc..deefb1a 100644
--- a/oauthlib/oauth2/__init__.py
+++ b/oauthlib/oauth2/__init__.py
@@ -33,3 +33,4 @@ from .rfc6749.grant_types import (
from .rfc6749.request_validator import RequestValidator
from .rfc6749.tokens import BearerToken, OAuth2Token
from .rfc6749.utils import is_secure_transport
+from .rfc8628.clients import DeviceClient
diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
index b799823..858855a 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
@@ -10,7 +10,6 @@ import logging
from oauthlib import common
from .. import errors
-from ..utils import is_secure_transport
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@@ -547,20 +546,3 @@ class AuthorizationCodeGrant(GrantTypeBase):
if challenge_method in self._code_challenge_methods:
return self._code_challenge_methods[challenge_method](verifier, challenge)
raise NotImplementedError('Unknown challenge_method %s' % challenge_method)
-
- def _create_cors_headers(self, request):
- """If CORS is allowed, create the appropriate headers."""
- if 'origin' not in request.headers:
- return {}
-
- origin = request.headers['origin']
- if not is_secure_transport(origin):
- log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
- return {}
- elif not self.request_validator.is_origin_allowed(
- request.client_id, origin, request):
- log.debug('Invalid origin "%s", CORS not allowed.', origin)
- return {}
- else:
- log.debug('Valid origin "%s", injecting CORS headers.', origin)
- return {'Access-Control-Allow-Origin': origin}
diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py
index a64f168..ca343a1 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/base.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/base.py
@@ -10,6 +10,7 @@ from oauthlib.oauth2.rfc6749 import errors, utils
from oauthlib.uri_validate import is_absolute_uri
from ..request_validator import RequestValidator
+from ..utils import is_secure_transport
log = logging.getLogger(__name__)
@@ -248,3 +249,20 @@ class GrantTypeBase:
raise errors.MissingRedirectURIError(request=request)
if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request)
+
+ def _create_cors_headers(self, request):
+ """If CORS is allowed, create the appropriate headers."""
+ if 'origin' not in request.headers:
+ return {}
+
+ origin = request.headers['origin']
+ if not is_secure_transport(origin):
+ log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
+ return {}
+ elif not self.request_validator.is_origin_allowed(
+ request.client_id, origin, request):
+ log.debug('Invalid origin "%s", CORS not allowed.', origin)
+ return {}
+ else:
+ log.debug('Valid origin "%s", injecting CORS headers.', origin)
+ return {'Access-Control-Allow-Origin': origin}
diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
index f801de4..ce33df0 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
@@ -69,6 +69,7 @@ class RefreshTokenGrant(GrantTypeBase):
log.debug('Issuing new token to client id %r (%r), %r.',
request.client_id, request.client, token)
+ headers.update(self._create_cors_headers(request))
return headers, json.dumps(token), 200
def validate_token_request(self, request):
diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py
index 610a708..02a13fa 100644
--- a/oauthlib/oauth2/rfc6749/request_validator.py
+++ b/oauthlib/oauth2/rfc6749/request_validator.py
@@ -671,6 +671,7 @@ class RequestValidator:
Method is used by:
- Authorization Code Grant
+ - Refresh Token Grant
"""
return False
diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py
index 6284248..0757d07 100644
--- a/oauthlib/oauth2/rfc6749/tokens.py
+++ b/oauthlib/oauth2/rfc6749/tokens.py
@@ -257,6 +257,7 @@ def get_token_from_header(request):
class TokenBase:
+ __slots__ = ()
def __call__(self, request, refresh_token=False):
raise NotImplementedError('Subclasses must implement this method.')
diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py
new file mode 100644
index 0000000..531929d
--- /dev/null
+++ b/oauthlib/oauth2/rfc8628/__init__.py
@@ -0,0 +1,10 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 Device Authorization RFC8628.
+"""
+import logging
+
+log = logging.getLogger(__name__)
diff --git a/oauthlib/oauth2/rfc8628/clients/__init__.py b/oauthlib/oauth2/rfc8628/clients/__init__.py
new file mode 100644
index 0000000..130b52e
--- /dev/null
+++ b/oauthlib/oauth2/rfc8628/clients/__init__.py
@@ -0,0 +1,8 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 Device Authorization RFC8628.
+"""
+from .device import DeviceClient
diff --git a/oauthlib/oauth2/rfc8628/clients/device.py b/oauthlib/oauth2/rfc8628/clients/device.py
new file mode 100644
index 0000000..95c4f5a
--- /dev/null
+++ b/oauthlib/oauth2/rfc8628/clients/device.py
@@ -0,0 +1,94 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 Device Authorization RFC8628.
+"""
+
+from oauthlib.oauth2 import BackendApplicationClient, Client
+from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
+from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
+from oauthlib.common import add_params_to_uri
+
+
+class DeviceClient(Client):
+
+ """A public client utilizing the device authorization workflow.
+
+ The client can request an access token using a device code and
+ a public client id associated with the device code as defined
+ in RFC8628.
+
+ The device authorization grant type can be used to obtain both
+ access tokens and refresh tokens and is intended to be used in
+ a scenario where the device being authorized does not have a
+ user interface that is suitable for performing authentication.
+ """
+
+ grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
+
+ def __init__(self, client_id, **kwargs):
+ super().__init__(client_id, **kwargs)
+ self.client_secret = kwargs.get('client_secret')
+
+ def prepare_request_uri(self, uri, scope=None, **kwargs):
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ scope = self.scope if scope is None else scope
+ params = [(('client_id', self.client_id)), (('grant_type', self.grant_type))]
+
+ if self.client_secret is not None:
+ params.append(('client_secret', self.client_secret))
+
+ if scope:
+ params.append(('scope', list_to_scope(scope)))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((str(k), kwargs[k]))
+
+ return add_params_to_uri(uri, params)
+
+ def prepare_request_body(self, device_code, body='', scope=None,
+ include_client_id=False, **kwargs):
+ """Add device_code to request body
+
+ The client makes a request to the token endpoint by adding the
+ device_code as a parameter using the
+ "application/x-www-form-urlencoded" format to the HTTP request
+ 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. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_. False otherwise (default).
+ :type include_client_id: Boolean
+
+ :param kwargs: Extra credentials to include in the token request.
+
+ The prepared body will include all provided device_code as well as
+ the ``grant_type`` parameter set to
+ ``urn:ietf:params:oauth:grant-type:device_code``::
+
+ >>> from oauthlib.oauth2 import DeviceClient
+ >>> client = DeviceClient('your_id', 'your_code')
+ >>> client.prepare_request_body(scope=['hello', 'world'])
+ 'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
+
+ .. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
+ """
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.grant_type, body=body, device_code=device_code,
+ scope=scope, **kwargs)
diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
index 1d3e77a..581f2a4 100644
--- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
+++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
@@ -18,6 +18,7 @@ class RefreshTokenGrantTest(TestCase):
self.request = Request('http://a.b/path')
self.request.grant_type = 'refresh_token'
self.request.refresh_token = 'lsdkfhj230'
+ self.request.client_id = 'abcdef'
self.request.client = mock_client
self.request.scope = 'foo'
self.mock_validator = mock.MagicMock()
@@ -168,3 +169,43 @@ class RefreshTokenGrantTest(TestCase):
del self.request.scope
self.auth.validate_token_request(self.request)
self.assertEqual(self.request.scopes, 'foo bar baz'.split())
+
+ # CORS
+
+ def test_create_cors_headers(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'https://foo.bar'
+ self.mock_validator.is_origin_allowed.return_value = True
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertEqual(
+ headers['Access-Control-Allow-Origin'], 'https://foo.bar'
+ )
+ self.mock_validator.is_origin_allowed.assert_called_once_with(
+ 'abcdef', 'https://foo.bar', self.request
+ )
+
+ def test_create_cors_headers_no_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_not_called()
+
+ def test_create_cors_headers_insecure_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'http://foo.bar'
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_not_called()
+
+ def test_create_cors_headers_invalid_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'https://foo.bar'
+ self.mock_validator.is_origin_allowed.return_value = False
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_called_once_with(
+ 'abcdef', 'https://foo.bar', self.request
+ )
diff --git a/tests/oauth2/rfc8628/__init__.py b/tests/oauth2/rfc8628/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/oauth2/rfc8628/__init__.py
diff --git a/tests/oauth2/rfc8628/clients/__init__.py b/tests/oauth2/rfc8628/clients/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/oauth2/rfc8628/clients/__init__.py
diff --git a/tests/oauth2/rfc8628/clients/test_device.py b/tests/oauth2/rfc8628/clients/test_device.py
new file mode 100644
index 0000000..725dea2
--- /dev/null
+++ b/tests/oauth2/rfc8628/clients/test_device.py
@@ -0,0 +1,63 @@
+import os
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2 import DeviceClient
+
+from tests.unittest import TestCase
+
+
+class DeviceClientTest(TestCase):
+
+ client_id = "someclientid"
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+
+ client_secret = "asecret"
+
+ device_code = "somedevicecode"
+
+ scope = ["profile", "email"]
+
+ body = "not=empty"
+
+ body_up = "not=empty&grant_type=urn:ietf:params:oauth:grant-type:device_code"
+ body_code = body_up + "&device_code=somedevicecode"
+ body_kwargs = body_code + "&some=providers&require=extra+arguments"
+
+ uri = "https://example.com/path?query=world"
+ uri_id = uri + "&client_id=" + client_id
+ uri_grant = uri_id + "&grant_type=urn:ietf:params:oauth:grant-type:device_code"
+ uri_secret = uri_grant + "&client_secret=asecret"
+ uri_scope = uri_secret + "&scope=profile+email"
+
+ def test_request_body(self):
+ client = DeviceClient(self.client_id)
+
+ # Basic, no extra arguments
+ body = client.prepare_request_body(self.device_code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ rclient = DeviceClient(self.client_id)
+ body = rclient.prepare_request_body(self.device_code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ # With extra parameters
+ body = client.prepare_request_body(
+ self.device_code, body=self.body, **self.kwargs)
+ self.assertFormBodyEqual(body, self.body_kwargs)
+
+ def test_request_uri(self):
+ client = DeviceClient(self.client_id)
+
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_grant)
+
+ client = DeviceClient(self.client_id, client_secret=self.client_secret)
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_secret)
+
+ uri = client.prepare_request_uri(self.uri, scope=self.scope)
+ self.assertURLEqual(uri, self.uri_scope)
diff --git a/tox.ini b/tox.ini
index 903f202..c072450 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py36,py37,py38,py39,py310,pypy3,docs,readme,bandit,isort
+envlist = py36,py37,py38,py39,py310,py311,pypy3,docs,readme,bandit,isort
[testenv]
deps=
@@ -9,9 +9,9 @@ commands=
# tox -e docs to mimick readthedocs build.
-# as of today, RTD is using python3.6 and doesn't run "setup.py install"
+# as of today, RTD is using python3.7 and doesn't run "setup.py install"
[testenv:docs]
-basepython=python3.6
+basepython=python3.7
skipsdist=True
deps=
sphinx