From 1152a61252568cd8abb7212782c0c0eff9504f82 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:49:54 +0100 Subject: [PATCH 1/9] Allow custom user_code --- .../rfc8628/endpoints/device_authorization.py | 7 +++-- tests/oauth2/rfc8628/test_server.py | 29 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 83c0dd74..35e8dcf4 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -39,6 +39,7 @@ def __init__( expires_in=1800, interval=None, verification_uri_complete=None, + user_code_generator = None ): """ :param request_validator: An instance of RequestValidator. @@ -49,11 +50,11 @@ def __init__( :param verification_uri_complete: a string of a function that can be called with `user_data` as parameter """ self.request_validator = request_validator + self.user_code_generator = user_code_generator self._expires_in = expires_in self._interval = interval self._verification_uri = verification_uri self._verification_uri_complete = verification_uri_complete - self._interval = interval BaseEndpoint.__init__(self) @@ -149,6 +150,8 @@ def create_device_authorization_response( :param uri: The full URI of the token request. :param request: OAuthlib request. + :param user_code_generator: A callable that returns a string for the user code + This allows the caller to decide how the user_code should be formatted :type request: oauthlib.common.Request :returns: A tuple of 3 elements. 1. A dict of headers to set on the response. @@ -205,7 +208,7 @@ def create_device_authorization_response( log.debug("Pre resource owner authorization validation ok for %r.", request) headers = {} - user_code = generate_token() + user_code = self.user_code_generator() if self.user_code_generator else generate_token() data = { "verification_uri": self.verification_uri, "expires_in": self.expires_in, diff --git a/tests/oauth2/rfc8628/test_server.py b/tests/oauth2/rfc8628/test_server.py index 7fc1d5a4..6267fd77 100644 --- a/tests/oauth2/rfc8628/test_server.py +++ b/tests/oauth2/rfc8628/test_server.py @@ -8,12 +8,13 @@ class DeviceAuthorizationEndpointTest(TestCase): - def _configure_endpoint(self, interval=None, verification_uri_complete=None): + def _configure_endpoint(self, interval=None, verification_uri_complete=None, user_code_generator=None): self.endpoint = DeviceAuthorizationEndpoint( request_validator=mock.MagicMock(spec=RequestValidator), verification_uri=self.verification_uri, interval=interval, verification_uri_complete=verification_uri_complete, + user_code_generator=user_code_generator ) def setUp(self): @@ -96,3 +97,29 @@ def test_device_authorization_grant_verify_url_complete_callable(self): "http://i.l/v?user_code=abc", json.loads(body)["verification_uri_complete"], ) + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_authorization_grant_user_gode_generator(self): + def user_code(): + """ + A friendly user code the device can display and the user + can type in. It's up to the device how + this code should be displayed. e.g 123-456 + """ + return "123456" + + self._configure_endpoint( + verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}", + user_code_generator=user_code + ) + + _, body, _ = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) + self.assertEqual( + "http://i.l/v?user_code=123456", + json.loads(body)["verification_uri_complete"], + ) From 465e8ac8762b53a24b9cc5c29c12adcd5b3ec781 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:47:16 +0100 Subject: [PATCH 2/9] Add convience import --- oauthlib/oauth2/rfc8628/__init__.py | 1 + oauthlib/openid/connect/core/endpoints/pre_configured.py | 1 + 2 files changed, 2 insertions(+) diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index 531929dc..73079aa8 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -6,5 +6,6 @@ for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ import logging +from .pre_configured import DeviceApplicationServer log = logging.getLogger(__name__) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 8cbc37df..5f582c87 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -15,6 +15,7 @@ ResourceOwnerPasswordCredentialsGrant, ) from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint from ..grant_types import ( AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant, From c90dc2e98d1e02d588cfee44f53b7ed2d11ca1dd Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:21:56 +0100 Subject: [PATCH 3/9] Allow user code to be configured in device auth flow Whilst oauth2 doesn't specify how the format should be the current behaviour generates a code that is not human and device friendly. e.g 6Pp9vPKaanbFydF9omtlNLLdJA4HG7 This commit makes it so that's the default behvaiour but allows the caller of DeviceApplicationServer to pass in a user code in a format they prefer in the form of a callable to be called that returns the code as a string --- oauthlib/oauth2/rfc8628/pre_configured.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/pre_configured.py index 3123d57f..71419e8a 100644 --- a/oauthlib/oauth2/rfc8628/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/pre_configured.py @@ -7,13 +7,14 @@ class DeviceApplicationServer(DeviceAuthorizationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" - def __init__(self, request_validator, verification_uri, **kwargs): + def __init__(self, request_validator, verification_uri, user_code_generator = None, **kwargs): """Construct a new web application server. :param request_validator: An implementation of oauthlib.oauth2.rfc8626.RequestValidator. :param verification_uri: the verification_uri to be send back. + :param user_code_generator: a callable that allows the user code to be configured. """ DeviceAuthorizationEndpoint.__init__( - self, request_validator, verification_uri=verification_uri + self, request_validator, verification_uri=verification_uri, user_code_generator=user_code_generator ) From ca2f4fe88bb3b58aef0ced2c36ae57f1c133c151 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:28:51 +0100 Subject: [PATCH 4/9] Return a python dict in create_device_authorization_response The json serialisation should occur at the interface level (e.g a view) that will use this method not the method itself as it can lead to "double" json serialisation or the need to use json.loads() to deserialize and serialise it again before the httpResponse is made back to the client --- oauthlib/oauth2/rfc8628/endpoints/device_authorization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 35e8dcf4..0adcb8a7 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -222,5 +222,4 @@ def create_device_authorization_response( if verification_uri_complete: data["verification_uri_complete"] = verification_uri_complete - body = json.dumps(data) - return headers, body, 200 + return headers, data, 200 From b1550fab24f608bb66c1a5326abf279f0ddd5e74 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:31:55 +0100 Subject: [PATCH 5/9] Update tests --- tests/oauth2/rfc8628/test_server.py | 55 +++++++++-------------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/tests/oauth2/rfc8628/test_server.py b/tests/oauth2/rfc8628/test_server.py index 6267fd77..0f3f8dc9 100644 --- a/tests/oauth2/rfc8628/test_server.py +++ b/tests/oauth2/rfc8628/test_server.py @@ -10,11 +10,7 @@ class DeviceAuthorizationEndpointTest(TestCase): def _configure_endpoint(self, interval=None, verification_uri_complete=None, user_code_generator=None): self.endpoint = DeviceAuthorizationEndpoint( - request_validator=mock.MagicMock(spec=RequestValidator), - verification_uri=self.verification_uri, - interval=interval, - verification_uri_complete=verification_uri_complete, - user_code_generator=user_code_generator + request_validator=mock.MagicMock(spec=RequestValidator), verification_uri=self.verification_uri, interval=interval, verification_uri_complete=verification_uri_complete, user_code_generator=user_code_generator ) def setUp(self): @@ -32,9 +28,7 @@ def response_payload(self): @mock.patch("oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token") def test_device_authorization_grant(self, generate_token): generate_token.side_effect = ["abc", "def"] - _, body, status_code = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) + _, body, status_code = self.endpoint.create_device_authorization_response(*self.response_payload()) expected_payload = { "verification_uri": "http://i.b/l/verify", "user_code": "abc", @@ -42,7 +36,7 @@ def test_device_authorization_grant(self, generate_token): "expires_in": 1800, } self.assertEqual(200, status_code) - self.assertEqual(json.loads(body), expected_payload) + self.assertEqual(body, expected_payload) @mock.patch( "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", @@ -50,10 +44,8 @@ def test_device_authorization_grant(self, generate_token): ) def test_device_authorization_grant_interval(self): self._configure_endpoint(interval=5) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) - self.assertEqual(5, json.loads(body)["interval"]) + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) + self.assertEqual(5, body["interval"]) @mock.patch( "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", @@ -61,25 +53,19 @@ def test_device_authorization_grant_interval(self): ) def test_device_authorization_grant_interval_with_zero(self): self._configure_endpoint(interval=0) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) - self.assertEqual(0, json.loads(body)["interval"]) + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) + self.assertEqual(0, body["interval"]) @mock.patch( "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", lambda: "abc", ) def test_device_authorization_grant_verify_url_complete_string(self): - self._configure_endpoint( - verification_uri_complete="http://i.l/v?user_code={user_code}" - ) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) + self._configure_endpoint(verification_uri_complete="http://i.l/v?user_code={user_code}") + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) self.assertEqual( "http://i.l/v?user_code=abc", - json.loads(body)["verification_uri_complete"], + body["verification_uri_complete"], ) @mock.patch( @@ -87,15 +73,11 @@ def test_device_authorization_grant_verify_url_complete_string(self): lambda: "abc", ) def test_device_authorization_grant_verify_url_complete_callable(self): - self._configure_endpoint( - verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}" - ) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) + self._configure_endpoint(verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}") + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) self.assertEqual( "http://i.l/v?user_code=abc", - json.loads(body)["verification_uri_complete"], + body["verification_uri_complete"], ) @mock.patch( @@ -111,15 +93,10 @@ def user_code(): """ return "123456" - self._configure_endpoint( - verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}", - user_code_generator=user_code - ) + self._configure_endpoint(verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}", user_code_generator=user_code) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) self.assertEqual( "http://i.l/v?user_code=123456", - json.loads(body)["verification_uri_complete"], + body["verification_uri_complete"], ) From ecdbcd3031f0b61ae7a045c56887630ec5da24ae Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:49:32 +0100 Subject: [PATCH 6/9] Add typing --- oauthlib/oauth2/rfc8628/endpoints/device_authorization.py | 3 ++- oauthlib/oauth2/rfc8628/pre_configured.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 0adcb8a7..702240e7 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -7,6 +7,7 @@ """ import json import logging +from typing import Callable from oauthlib.common import Request, generate_token from oauthlib.oauth2.rfc6749 import errors @@ -39,7 +40,7 @@ def __init__( expires_in=1800, interval=None, verification_uri_complete=None, - user_code_generator = None + user_code_generator: Callable[[None], str] = None ): """ :param request_validator: An instance of RequestValidator. diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/pre_configured.py index 71419e8a..ad910159 100644 --- a/oauthlib/oauth2/rfc8628/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/pre_configured.py @@ -1,13 +1,14 @@ from oauthlib.oauth2.rfc8628.endpoints.device_authorization import ( DeviceAuthorizationEndpoint, ) +from typing import Callable class DeviceApplicationServer(DeviceAuthorizationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" - def __init__(self, request_validator, verification_uri, user_code_generator = None, **kwargs): + def __init__(self, request_validator, verification_uri, user_code_generator: Callable[[None], str] = None, **kwargs): """Construct a new web application server. :param request_validator: An implementation of From 68efb8ce4e22188fbbf2e9ae08d72d253f72b77c Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:12:56 +0100 Subject: [PATCH 7/9] Apply ruff format --- oauthlib/oauth2/rfc8628/__init__.py | 1 + .../rfc8628/endpoints/device_authorization.py | 27 ++---- oauthlib/oauth2/rfc8628/pre_configured.py | 5 +- .../connect/core/endpoints/pre_configured.py | 85 ++++++++++--------- 4 files changed, 55 insertions(+), 63 deletions(-) diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index 73079aa8..12f9893b 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -5,6 +5,7 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ + import logging from .pre_configured import DeviceApplicationServer diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 702240e7..2b0bb3e7 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -5,6 +5,7 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 RFC8628. """ + import json import logging from typing import Callable @@ -12,14 +13,14 @@ from oauthlib.common import Request, generate_token from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.endpoints.base import ( - BaseEndpoint, catch_errors_and_unavailability, + BaseEndpoint, + catch_errors_and_unavailability, ) log = logging.getLogger(__name__) class DeviceAuthorizationEndpoint(BaseEndpoint): - """DeviceAuthorization endpoint - used by the client to initiate the authorization flow by requesting a set of verification codes from the authorization server by making an HTTP "POST" request to @@ -33,15 +34,7 @@ class DeviceAuthorizationEndpoint(BaseEndpoint): themselves. """ - def __init__( - self, - request_validator, - verification_uri, - expires_in=1800, - interval=None, - verification_uri_complete=None, - user_code_generator: Callable[[None], str] = None - ): + def __init__(self, request_validator, verification_uri, expires_in=1800, interval=None, verification_uri_complete=None, user_code_generator: Callable[[None], str] = None): """ :param request_validator: An instance of RequestValidator. :type request_validator: oauthlib.oauth2.rfc6749.RequestValidator. @@ -106,13 +99,9 @@ def validate_device_authorization_request(self, request): try: duplicate_params = request.duplicate_params except ValueError: - raise errors.InvalidRequestFatalError( - description="Unable to parse query string", request=request - ) + raise errors.InvalidRequestFatalError(description="Unable to parse query string", request=request) if param in duplicate_params: - raise errors.InvalidRequestFatalError( - description="Duplicate %s parameter." % param, request=request - ) + raise errors.InvalidRequestFatalError(description="Duplicate %s parameter." % param, request=request) # the "application/x-www-form-urlencoded" format, per Appendix B of [RFC6749] # https://www.rfc-editor.org/rfc/rfc6749#appendix-B @@ -140,9 +129,7 @@ def validate_device_authorization_request(self, request): self._raise_on_invalid_client(request) @catch_errors_and_unavailability - def create_device_authorization_response( - self, uri, http_method="POST", body=None, headers=None - ): + def create_device_authorization_response(self, uri, http_method="POST", body=None, headers=None): """create_device_authorization_response - generates a unique device verification code and an end-user code that are valid for a limited time and includes them in the HTTP response body using the diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/pre_configured.py index ad910159..2eef0b99 100644 --- a/oauthlib/oauth2/rfc8628/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/pre_configured.py @@ -5,7 +5,6 @@ class DeviceApplicationServer(DeviceAuthorizationEndpoint): - """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" def __init__(self, request_validator, verification_uri, user_code_generator: Callable[[None], str] = None, **kwargs): @@ -16,6 +15,4 @@ def __init__(self, request_validator, verification_uri, user_code_generator: Cal :param verification_uri: the verification_uri to be send back. :param user_code_generator: a callable that allows the user code to be configured. """ - DeviceAuthorizationEndpoint.__init__( - self, request_validator, verification_uri=verification_uri, user_code_generator=user_code_generator - ) + DeviceAuthorizationEndpoint.__init__(self, request_validator, verification_uri=verification_uri, user_code_generator=user_code_generator) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 5f582c87..2c2a9ce4 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -5,37 +5,42 @@ This module is an implementation of various endpoints needed for providing OpenID Connect servers. """ + from oauthlib.oauth2.rfc6749.endpoints import ( - AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint, - RevocationEndpoint, TokenEndpoint, + AuthorizationEndpoint, + IntrospectEndpoint, + ResourceEndpoint, + RevocationEndpoint, + TokenEndpoint, ) from oauthlib.oauth2.rfc6749.grant_types import ( AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, - ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant, + ClientCredentialsGrant, + ImplicitGrant as OAuth2ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ) from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint from ..grant_types import ( - AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant, + AuthorizationCodeGrant, + HybridGrant, + ImplicitGrant, + RefreshTokenGrant, ) from ..grant_types.dispatchers import ( - AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher, + AuthorizationCodeGrantDispatcher, + AuthorizationTokenGrantDispatcher, ImplicitTokenGrantDispatcher, ) from ..tokens import JWTToken from .userinfo import UserInfoEndpoint -class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint): - +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint): """An all-in-one endpoint featuring all four major grant types.""" - def __init__(self, request_validator, token_expires_in=None, - token_generator=None, refresh_token_generator=None, - *args, **kwargs): + def __init__(self, request_validator, token_expires_in=None, token_generator=None, refresh_token_generator=None, *args, **kwargs): """Construct a new all-grants-in-one server. :param request_validator: An implementation of @@ -51,50 +56,52 @@ def __init__(self, request_validator, token_expires_in=None, """ self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator) self.implicit_grant = OAuth2ImplicitGrant(request_validator) - self.password_grant = ResourceOwnerPasswordCredentialsGrant( - request_validator) + self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) self.openid_connect_auth = AuthorizationCodeGrant(request_validator) self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) + self.bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) - self.jwt = JWTToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) + self.jwt = JWTToken(request_validator, token_generator, token_expires_in, refresh_token_generator) self.auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) self.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit) # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={ - 'code': self.auth_grant_choice, - 'token': self.implicit_grant_choice, - 'id_token': self.openid_connect_implicit, - 'id_token token': self.openid_connect_implicit, - 'code token': self.openid_connect_hybrid, - 'code id_token': self.openid_connect_hybrid, - 'code id_token token': self.openid_connect_hybrid, - 'none': self.auth_grant - }, - default_token_type=self.bearer) + AuthorizationEndpoint.__init__( + self, + default_response_type="code", + response_types={ + "code": self.auth_grant_choice, + "token": self.implicit_grant_choice, + "id_token": self.openid_connect_implicit, + "id_token token": self.openid_connect_implicit, + "code token": self.openid_connect_hybrid, + "code id_token": self.openid_connect_hybrid, + "code id_token token": self.openid_connect_hybrid, + "none": self.auth_grant, + }, + default_token_type=self.bearer, + ) self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': self.token_grant_choice, - 'password': self.password_grant, - 'client_credentials': self.credentials_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer, 'JWT': self.jwt}) + TokenEndpoint.__init__( + self, + default_grant_type="authorization_code", + grant_types={ + "authorization_code": self.token_grant_choice, + "password": self.password_grant, + "client_credentials": self.credentials_grant, + "refresh_token": self.refresh_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__(self, default_token="Bearer", token_types={"Bearer": self.bearer, "JWT": self.jwt}) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) UserInfoEndpoint.__init__(self, request_validator) From 24c8a21b6cff9f5cf60901605c4769601a5d3f47 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:54:56 +0100 Subject: [PATCH 8/9] Add device authorization doc --- docs/oauth2/endpoints/device.rst | 55 +++++++ docs/oauth2/endpoints/endpoints.rst | 1 + docs/oauth2/preconfigured_servers.rst | 4 + oauthlib/oauth2/__init__.py | 2 +- oauthlib/oauth2/rfc8628/__init__.py | 1 - oauthlib/oauth2/rfc8628/endpoints/__init__.py | 1 + .../rfc8628/endpoints/device_authorization.py | 150 ++++++++++-------- .../rfc8628/{ => endpoints}/pre_configured.py | 0 .../rfc8628/endpoints/test_error_responses.py | 2 +- 9 files changed, 147 insertions(+), 69 deletions(-) create mode 100644 docs/oauth2/endpoints/device.rst rename oauthlib/oauth2/rfc8628/{ => endpoints}/pre_configured.py (100%) diff --git a/docs/oauth2/endpoints/device.rst b/docs/oauth2/endpoints/device.rst new file mode 100644 index 00000000..728fd3c2 --- /dev/null +++ b/docs/oauth2/endpoints/device.rst @@ -0,0 +1,55 @@ +============= +Device +============= + +The device endpoint is used to initiate the authorization flow by requesting a set of +verification codes from the authorization server by making an HTTP "POST" request to +the device authorization endpoint. + +** Device Authorization Request ** + The client makes a device authorization request to the device + authorization endpoint by including the following parameters using + the "application/x-www-form-urlencoded" format: + + POST /device_authorization HTTP/1.1 + Host: server.example.com + Content-Type: application/x-www-form-urlencoded + client_id=123456&scope=example_scope + +.. code-block:: python + + # Initial setup + from your_validator import your_validator + verification_uri = "https://example.com/device" + + def user_code(): + # some logic to generate a random string... + return "123-456" + + # user code is optional + server = DeviceApplicationServer(your_validator, verification_uri, user_code) + + headers, data, status = server.create_device_authorization_response(request) + + # response from /device_authorization endpoint on your server + from your_framework import http_response + http_response(data, status=status, headers=headers) + + + +.. code-block:: python + + # example response + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "123-456", + "verification_uri": "https://example.com/device", + "verification_uri_complete": + "https://example.com/device?user_code=WDJB-MJHT", + "expires_in": 1800, + "interval": 5 + } + + +.. autoclass:: oauthlib.oauth2.DeviceAuthorizationEndpoint + :members: diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index f05c44b6..d2a4a07a 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -15,6 +15,7 @@ client attempts to access the user resources on their behalf. :maxdepth: 2 authorization + device introspect token metadata diff --git a/docs/oauth2/preconfigured_servers.rst b/docs/oauth2/preconfigured_servers.rst index e1f629c2..a32cf8a1 100644 --- a/docs/oauth2/preconfigured_servers.rst +++ b/docs/oauth2/preconfigured_servers.rst @@ -36,3 +36,7 @@ This function is passed the request object and a boolean indicating whether to g .. autoclass:: oauthlib.oauth2.BackendApplicationServer :members: + + +.. autoclass:: oauthlib.oauth2.DeviceApplicationServer + :members: diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index fe4ca80b..f32fbcae 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -34,4 +34,4 @@ from .rfc6749.tokens import BearerToken, OAuth2Token from .rfc6749.utils import is_secure_transport from .rfc8628.clients import DeviceClient -from .rfc8628.endpoints import DeviceAuthorizationEndpoint +from .rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index 12f9893b..6c3d14af 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -7,6 +7,5 @@ """ import logging -from .pre_configured import DeviceApplicationServer log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc8628/endpoints/__init__.py b/oauthlib/oauth2/rfc8628/endpoints/__init__.py index 75eef2b7..7717699b 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc8628/endpoints/__init__.py @@ -6,3 +6,4 @@ for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ from .device_authorization import DeviceAuthorizationEndpoint +from .pre_configured import DeviceApplicationServer diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 2b0bb3e7..7cda5ee1 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -6,7 +6,6 @@ for consuming and providing OAuth 2.0 RFC8628. """ -import json import logging from typing import Callable @@ -34,7 +33,15 @@ class DeviceAuthorizationEndpoint(BaseEndpoint): themselves. """ - def __init__(self, request_validator, verification_uri, expires_in=1800, interval=None, verification_uri_complete=None, user_code_generator: Callable[[None], str] = None): + def __init__( + self, + request_validator, + verification_uri, + expires_in=1800, + interval=None, + verification_uri_complete=None, + user_code_generator: Callable[[None], str] = None, + ): """ :param request_validator: An instance of RequestValidator. :type request_validator: oauthlib.oauth2.rfc6749.RequestValidator. @@ -42,13 +49,14 @@ def __init__(self, request_validator, verification_uri, expires_in=1800, interva :param expires_in: a number that represents the lifetime of the `user_code` and `device_code` :param interval: an option number that represents the number of seconds between each poll requests :param verification_uri_complete: a string of a function that can be called with `user_data` as parameter + :param user_code_generator: a callable that returns a configurable user code """ self.request_validator = request_validator - self.user_code_generator = user_code_generator self._expires_in = expires_in self._interval = interval self._verification_uri = verification_uri self._verification_uri_complete = verification_uri_complete + self.user_code_generator = user_code_generator BaseEndpoint.__init__(self) @@ -99,9 +107,13 @@ def validate_device_authorization_request(self, request): try: duplicate_params = request.duplicate_params except ValueError: - raise errors.InvalidRequestFatalError(description="Unable to parse query string", request=request) + raise errors.InvalidRequestFatalError( + description="Unable to parse query string", request=request + ) if param in duplicate_params: - raise errors.InvalidRequestFatalError(description="Duplicate %s parameter." % param, request=request) + raise errors.InvalidRequestFatalError( + description="Duplicate %s parameter." % param, request=request + ) # the "application/x-www-form-urlencoded" format, per Appendix B of [RFC6749] # https://www.rfc-editor.org/rfc/rfc6749#appendix-B @@ -129,68 +141,74 @@ def validate_device_authorization_request(self, request): self._raise_on_invalid_client(request) @catch_errors_and_unavailability - def create_device_authorization_response(self, uri, http_method="POST", body=None, headers=None): - """create_device_authorization_response - generates a unique device - verification code and an end-user code that are valid for a limited - time and includes them in the HTTP response body using the - "application/json" format [RFC8259] with a 200 (OK) status code, as - described in `Section-3.2`_. - - :param uri: The full URI of the token request. - :param request: OAuthlib request. - :param user_code_generator: A callable that returns a string for the user code - This allows the caller to decide how the user_code should be formatted - :type request: oauthlib.common.Request - :returns: A tuple of 3 elements. - 1. A dict of headers to set on the response. - 2. The response body as a string. - 3. The response status code as an integer. - - The response contains the following parameters: - - device_code - REQUIRED. The device verification code. - - user_code - REQUIRED. The end-user verification code. - - verification_uri - REQUIRED. The end-user verification URI on the authorization - server. The URI should be short and easy to remember as end users - will be asked to manually type it into their user agent. - - verification_uri_complete - OPTIONAL. A verification URI that includes the "user_code" (or - other information with the same function as the "user_code"), - which is designed for non-textual transmission. - - expires_in - REQUIRED. The lifetime in seconds of the "device_code" and - "user_code". - - interval - OPTIONAL. The minimum amount of time in seconds that the client - SHOULD wait between polling requests to the token endpoint. If no - value is provided, clients MUST use 5 as the default. - - For example: - - HTTP/1.1 200 OK - Content-Type: application/json - Cache-Control: no-store - - { - "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", - "user_code": "WDJB-MJHT", - "verification_uri": "https://example.com/device", - "verification_uri_complete": - "https://example.com/device?user_code=WDJB-MJHT", - "expires_in": 1800, - "interval": 5 - } - - .. _`Section-3.2`: https://www.rfc-editor.org/rfc/rfc8628#section-3.2 + def create_device_authorization_response( + self, uri, http_method="POST", body=None, headers=None + ): """ + Generate a unique device verification code and an end-user code that are valid for a limited time. + Include them in the HTTP response body using the "application/json" format [RFC8259] with a + 200 (OK) status code, as described in `Section-3.2`_. + + :param uri: The full URI of the token request. + :type uri: str + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param user_code_generator: + A callable that returns a string for the user code. + This allows the caller to decide how the `user_code` should be formatted. + :type user_code_generator: Callable[[], str] + :return: A tuple of three elements: + 1. A dict of headers to set on the response. + 2. The response body as a string. + 3. The response status code as an integer. + :rtype: tuple + + The response contains the following parameters: + + device_code + **REQUIRED.** The device verification code. + + user_code + **REQUIRED.** The end-user verification code. + + verification_uri + **REQUIRED.** The end-user verification URI on the authorization server. + The URI should be short and easy to remember as end users will be asked + to manually type it into their user agent. + + verification_uri_complete + **OPTIONAL.** A verification URI that includes the `user_code` (or + other information with the same function as the `user_code`), which is + designed for non-textual transmission. + + expires_in + **REQUIRED.** The lifetime in seconds of the `device_code` and `user_code`. + + interval + **OPTIONAL.** The minimum amount of time in seconds that the client + SHOULD wait between polling requests to the token endpoint. If no + value is provided, clients MUST use 5 as the default. + + **For example:** + + .. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + Cache-Control: no-store + + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://example.com/device", + "verification_uri_complete": + "https://example.com/device?user_code=WDJB-MJHT", + "expires_in": 1800, + "interval": 5 + } + + .. _`Section-3.2`: https://www.rfc-editor.org/rfc/rfc8628#section-3.2 + """ request = Request(uri, http_method, body, headers) self.validate_device_authorization_request(request) log.debug("Pre resource owner authorization validation ok for %r.", request) diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py similarity index 100% rename from oauthlib/oauth2/rfc8628/pre_configured.py rename to oauthlib/oauth2/rfc8628/endpoints/pre_configured.py diff --git a/tests/oauth2/rfc8628/endpoints/test_error_responses.py b/tests/oauth2/rfc8628/endpoints/test_error_responses.py index 16f05961..f92a7a84 100644 --- a/tests/oauth2/rfc8628/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc8628/endpoints/test_error_responses.py @@ -3,7 +3,7 @@ from oauthlib.common import Request, urlencode from oauthlib.oauth2.rfc6749 import errors -from oauthlib.oauth2.rfc8628.pre_configured import DeviceApplicationServer +from oauthlib.oauth2.rfc8628.endpoints.pre_configured import DeviceApplicationServer from oauthlib.oauth2.rfc8628.request_validator import RequestValidator From 6862c41f3994f770c3a1fbdb399a689f6e98a520 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:34:46 +0100 Subject: [PATCH 9/9] Apply 99 line length ruff format --- oauthlib/oauth2/__init__.py | 64 ++++++++++++++----- oauthlib/oauth2/rfc8628/endpoints/__init__.py | 1 + .../rfc8628/endpoints/pre_configured.py | 15 ++++- .../connect/core/endpoints/pre_configured.py | 43 ++++++++++--- .../rfc8628/endpoints/test_error_responses.py | 8 +-- tests/oauth2/rfc8628/test_server.py | 19 ++++-- 6 files changed, 114 insertions(+), 36 deletions(-) diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index f32fbcae..da9ccf3d 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -5,30 +5,62 @@ This module is a wrapper for the most recent implementation of OAuth 2.0 Client and Server classes. """ + from .rfc6749.clients import ( - BackendApplicationClient, Client, LegacyApplicationClient, - MobileApplicationClient, ServiceApplicationClient, WebApplicationClient, + BackendApplicationClient, + Client, + LegacyApplicationClient, + MobileApplicationClient, + ServiceApplicationClient, + WebApplicationClient, ) from .rfc6749.endpoints import ( - AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint, - LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer, - ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint, + AuthorizationEndpoint, + BackendApplicationServer, + IntrospectEndpoint, + LegacyApplicationServer, + MetadataEndpoint, + MobileApplicationServer, + ResourceEndpoint, + RevocationEndpoint, + Server, + TokenEndpoint, WebApplicationServer, ) from .rfc6749.errors import ( - AccessDeniedError, FatalClientError, InsecureTransportError, - InvalidClientError, InvalidClientIdError, InvalidGrantError, - InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, - InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, - MissingClientIdError, MissingCodeError, MissingRedirectURIError, - MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, - OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError, - UnauthorizedClientError, UnsupportedGrantTypeError, - UnsupportedResponseTypeError, UnsupportedTokenTypeError, + AccessDeniedError, + FatalClientError, + InsecureTransportError, + InvalidClientError, + InvalidClientIdError, + InvalidGrantError, + InvalidRedirectURIError, + InvalidRequestError, + InvalidRequestFatalError, + InvalidScopeError, + MismatchingRedirectURIError, + MismatchingStateError, + MissingClientIdError, + MissingCodeError, + MissingRedirectURIError, + MissingResponseTypeError, + MissingTokenError, + MissingTokenTypeError, + OAuth2Error, + ServerError, + TemporarilyUnavailableError, + TokenExpiredError, + UnauthorizedClientError, + UnsupportedGrantTypeError, + UnsupportedResponseTypeError, + UnsupportedTokenTypeError, ) from .rfc6749.grant_types import ( - AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, + AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant, ) from .rfc6749.request_validator import RequestValidator from .rfc6749.tokens import BearerToken, OAuth2Token diff --git a/oauthlib/oauth2/rfc8628/endpoints/__init__.py b/oauthlib/oauth2/rfc8628/endpoints/__init__.py index 7717699b..dc834797 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc8628/endpoints/__init__.py @@ -5,5 +5,6 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ + from .device_authorization import DeviceAuthorizationEndpoint from .pre_configured import DeviceApplicationServer diff --git a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index 2eef0b99..cdb6b313 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -7,7 +7,13 @@ class DeviceApplicationServer(DeviceAuthorizationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" - def __init__(self, request_validator, verification_uri, user_code_generator: Callable[[None], str] = None, **kwargs): + def __init__( + self, + request_validator, + verification_uri, + user_code_generator: Callable[[None], str] = None, + **kwargs, + ): """Construct a new web application server. :param request_validator: An implementation of @@ -15,4 +21,9 @@ def __init__(self, request_validator, verification_uri, user_code_generator: Cal :param verification_uri: the verification_uri to be send back. :param user_code_generator: a callable that allows the user code to be configured. """ - DeviceAuthorizationEndpoint.__init__(self, request_validator, verification_uri=verification_uri, user_code_generator=user_code_generator) + DeviceAuthorizationEndpoint.__init__( + self, + request_validator, + verification_uri=verification_uri, + user_code_generator=user_code_generator, + ) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 2c2a9ce4..7c9393e6 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -37,10 +37,25 @@ from .userinfo import UserInfoEndpoint -class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint): +class Server( + AuthorizationEndpoint, + IntrospectEndpoint, + TokenEndpoint, + ResourceEndpoint, + RevocationEndpoint, + UserInfoEndpoint, +): """An all-in-one endpoint featuring all four major grant types.""" - def __init__(self, request_validator, token_expires_in=None, token_generator=None, refresh_token_generator=None, *args, **kwargs): + def __init__( + self, + request_validator, + token_expires_in=None, + token_generator=None, + refresh_token_generator=None, + *args, + **kwargs, + ): """Construct a new all-grants-in-one server. :param request_validator: An implementation of @@ -63,12 +78,20 @@ def __init__(self, request_validator, token_expires_in=None, token_generator=Non self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) - self.jwt = JWTToken(request_validator, token_generator, token_expires_in, refresh_token_generator) + self.jwt = JWTToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) - self.auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) - self.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit) + self.auth_grant_choice = AuthorizationCodeGrantDispatcher( + default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth + ) + self.implicit_grant_choice = ImplicitTokenGrantDispatcher( + default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit + ) # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination @@ -88,7 +111,9 @@ def __init__(self, request_validator, token_expires_in=None, token_generator=Non default_token_type=self.bearer, ) - self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) + self.token_grant_choice = AuthorizationTokenGrantDispatcher( + request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth + ) TokenEndpoint.__init__( self, @@ -101,7 +126,9 @@ def __init__(self, request_validator, token_expires_in=None, token_generator=Non }, default_token_type=self.bearer, ) - ResourceEndpoint.__init__(self, default_token="Bearer", token_types={"Bearer": self.bearer, "JWT": self.jwt}) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer, "JWT": self.jwt} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) UserInfoEndpoint.__init__(self, request_validator) diff --git a/tests/oauth2/rfc8628/endpoints/test_error_responses.py b/tests/oauth2/rfc8628/endpoints/test_error_responses.py index f92a7a84..c799cc81 100644 --- a/tests/oauth2/rfc8628/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc8628/endpoints/test_error_responses.py @@ -13,9 +13,7 @@ def set_client(self, request): request.client.client_id = "mocked" return True - def build_request( - self, uri="https://example.com/device_authorize", client_id="foo" - ): + def build_request(self, uri="https://example.com/device_authorize", client_id="foo"): body = "" if client_id: body = f"client_id={client_id}" @@ -46,9 +44,7 @@ def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) self.validator.get_default_redirect_uri.return_value = None self.validator.get_code_challenge.return_value = None - self.device = DeviceApplicationServer( - self.validator, "https://example.com/verify" - ) + self.device = DeviceApplicationServer(self.validator, "https://example.com/verify") def test_missing_client_id(self): # Device code grant diff --git a/tests/oauth2/rfc8628/test_server.py b/tests/oauth2/rfc8628/test_server.py index 0f3f8dc9..52025032 100644 --- a/tests/oauth2/rfc8628/test_server.py +++ b/tests/oauth2/rfc8628/test_server.py @@ -8,9 +8,15 @@ class DeviceAuthorizationEndpointTest(TestCase): - def _configure_endpoint(self, interval=None, verification_uri_complete=None, user_code_generator=None): + def _configure_endpoint( + self, interval=None, verification_uri_complete=None, user_code_generator=None + ): self.endpoint = DeviceAuthorizationEndpoint( - request_validator=mock.MagicMock(spec=RequestValidator), verification_uri=self.verification_uri, interval=interval, verification_uri_complete=verification_uri_complete, user_code_generator=user_code_generator + request_validator=mock.MagicMock(spec=RequestValidator), + verification_uri=self.verification_uri, + interval=interval, + verification_uri_complete=verification_uri_complete, + user_code_generator=user_code_generator, ) def setUp(self): @@ -28,7 +34,9 @@ def response_payload(self): @mock.patch("oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token") def test_device_authorization_grant(self, generate_token): generate_token.side_effect = ["abc", "def"] - _, body, status_code = self.endpoint.create_device_authorization_response(*self.response_payload()) + _, body, status_code = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) expected_payload = { "verification_uri": "http://i.b/l/verify", "user_code": "abc", @@ -93,7 +101,10 @@ def user_code(): """ return "123456" - self._configure_endpoint(verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}", user_code_generator=user_code) + self._configure_endpoint( + verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}", + user_code_generator=user_code, + ) _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) self.assertEqual(
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: