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..da9ccf3d 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -5,33 +5,65 @@ 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 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 531929dc..6c3d14af 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 log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc8628/endpoints/__init__.py b/oauthlib/oauth2/rfc8628/endpoints/__init__.py index 75eef2b7..dc834797 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc8628/endpoints/__init__.py @@ -5,4 +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/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 83c0dd74..7cda5ee1 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -5,20 +5,21 @@ 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 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 @@ -39,6 +40,7 @@ def __init__( expires_in=1800, interval=None, verification_uri_complete=None, + user_code_generator: Callable[[None], str] = None, ): """ :param request_validator: An instance of RequestValidator. @@ -47,13 +49,14 @@ def __init__( :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._expires_in = expires_in self._interval = interval self._verification_uri = verification_uri self._verification_uri_complete = verification_uri_complete - self._interval = interval + self.user_code_generator = user_code_generator BaseEndpoint.__init__(self) @@ -141,71 +144,77 @@ def validate_device_authorization_request(self, request): 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. - :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 """ + 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) 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, @@ -219,5 +228,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 diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py similarity index 56% rename from oauthlib/oauth2/rfc8628/pre_configured.py rename to oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index 3123d57f..cdb6b313 100644 --- a/oauthlib/oauth2/rfc8628/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -1,19 +1,29 @@ 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, **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 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, ) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 8cbc37df..7c9393e6 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -5,36 +5,57 @@ 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 @@ -50,50 +71,64 @@ 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) + 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) + 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) diff --git a/tests/oauth2/rfc8628/endpoints/test_error_responses.py b/tests/oauth2/rfc8628/endpoints/test_error_responses.py index 16f05961..c799cc81 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 @@ -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 7fc1d5a4..52025032 100644 --- a/tests/oauth2/rfc8628/test_server.py +++ b/tests/oauth2/rfc8628/test_server.py @@ -8,12 +8,15 @@ 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): @@ -41,7 +44,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", @@ -49,10 +52,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", @@ -60,25 +61,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( @@ -86,13 +81,33 @@ 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}" + 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", + body["verification_uri_complete"], ) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() + + @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=abc", - json.loads(body)["verification_uri_complete"], + "http://i.l/v?user_code=123456", + body["verification_uri_complete"], ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy