From 0a2e6e826fa5b034b7e5b038244d82152a4f387f Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:30:26 +0000 Subject: [PATCH 01/12] Add device grant specific errors These are from section 3.5 of the rfc https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 --- oauthlib/oauth2/rfc8628/__init__.py | 1 + oauthlib/oauth2/rfc8628/errors.py | 48 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 oauthlib/oauth2/rfc8628/errors.py diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index 6c3d14af..f152d41a 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -6,6 +6,7 @@ for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ +from oauthlib.oauth2.rfc8628.errors import SlowDownError, AuthorizationPendingError, ExpiredTokenError import logging log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc8628/errors.py b/oauthlib/oauth2/rfc8628/errors.py new file mode 100644 index 00000000..0bc0feb2 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/errors.py @@ -0,0 +1,48 @@ +from oauthlib.oauth2.rfc6749.errors import OAuth2Error + +""" +oauthlib.oauth2.rfc8628.errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Error used both by OAuth2 clients and providers to represent the spec +defined error responses specific to the the device grant +""" + +class AuthorizationPendingError(OAuth2Error): + """ + For the device authorization grant; + The authorization request is still pending as the end user hasn't + yet completed the user-interaction steps (Section 3.3). The + client SHOULD repeat the access token request to the token + endpoint (a process known as polling). Before each new request, + the client MUST wait at least the number of seconds specified by + the "interval" parameter of the device authorization response, + or 5 seconds if none was provided, and respect any + increase in the polling interval required by the "slow_down" + error. + """ + error = 'authorization_pending' + + +class SlowDownError(OAuth2Error): + """ + A variant of "authorization_pending", the authorization request is + still pending and polling should continue, but the interval MUST + be increased by 5 seconds for this and all subsequent requests. + """ + error = 'slow_down' + +class ExpiredTokenError(OAuth2Error): + """ + The "device_code" has expired, and the device authorization + session has concluded. The client MAY commence a new device + authorization request but SHOULD wait for user interaction before + restarting to avoid unnecessary polling. + """ + error = 'expired_token' + +class AccessDenied(OAuth2Error): + """ + The authorization request was denied. + """ + error = 'access_denied' From 82e22ebd5ed80ebbab5f7d4fd5a8555590bedbb4 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:33:49 +0000 Subject: [PATCH 02/12] Add device code grant handler In the device flow, the device must poll the token endpoint to check if the user has submitted the user_code. This commit adds that handler to validate the request and return the token. --- oauthlib/oauth2/__init__.py | 1 + .../oauth2/rfc8628/grant_types/__init__.py | 1 + .../oauth2/rfc8628/grant_types/device_code.py | 111 ++++++++++++++++++ .../connect/core/endpoints/pre_configured.py | 9 +- 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 oauthlib/oauth2/rfc8628/grant_types/__init__.py create mode 100644 oauthlib/oauth2/rfc8628/grant_types/device_code.py diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index da9ccf3d..2630f694 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -67,3 +67,4 @@ from .rfc6749.utils import is_secure_transport from .rfc8628.clients import DeviceClient from .rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant diff --git a/oauthlib/oauth2/rfc8628/grant_types/__init__.py b/oauthlib/oauth2/rfc8628/grant_types/__init__.py new file mode 100644 index 00000000..418dba77 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/grant_types/__init__.py @@ -0,0 +1 @@ +from oauthlib.oauth2.rfc8628.grant_types.device_code import DeviceCodeGrant diff --git a/oauthlib/oauth2/rfc8628/grant_types/device_code.py b/oauthlib/oauth2/rfc8628/grant_types/device_code.py new file mode 100644 index 00000000..8e4393d3 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/grant_types/device_code.py @@ -0,0 +1,111 @@ +from __future__ import annotations +import json + +from typing import Callable + +from oauthlib import common # noqa: TCH001 + +from oauthlib.oauth2.rfc6749 import errors as rfc6749_errors +from oauthlib.oauth2.rfc6749.grant_types.base import GrantTypeBase + + +class DeviceCodeGrant(GrantTypeBase): + def create_authorization_response( + self, request: common.Request, token_handler: Callable + ) -> tuple[dict, str, int]: + """ + Validate the device flow request -> create the access token + -> persist the token -> return the token. + """ + headers = self._get_default_headers() + try: + self.validate_token_request(request) + except rfc6749_errors.OAuth2Error as e: + headers.update(e.headers) + return headers, e.json, e.status_code + + token = token_handler.create_token(request, refresh_token=False) + + for modifier in self._token_modifiers: + token = modifier(token) + + self.request_validator.save_token(token, request) + + return self.create_token_response(request, token_handler) + + def validate_token_request(self, request: common.Request) -> None: + """ + Performs the necessary check against the request to ensure + it's allowed to retrieve a token. + """ + for validator in self.custom_validators.pre_token: + validator(request) + + if not getattr(request, "grant_type", None): + raise rfc6749_errors.InvalidRequestError( + "Request is missing grant type.", request=request + ) + + if request.grant_type != "urn:ietf:params:oauth:grant-type:device_code": + raise rfc6749_errors.UnsupportedGrantTypeError(request=request) + + for param in ("grant_type", "scope"): + if param in request.duplicate_params: + raise rfc6749_errors.InvalidRequestError( + description=f"Duplicate {param} parameter.", request=request + ) + + if not self.request_validator.authenticate_client(request): + raise rfc6749_errors.InvalidClientError(request=request) + elif not hasattr(request.client, "client_id"): + raise NotImplementedError( + "Authenticate client must set the " + "request.client.client_id attribute " + "in authenticate_client." + ) + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + request.client_id = request.client_id or request.client.client_id + self.validate_scopes(request) + + for validator in self.custom_validators.post_token: + validator(request) + + def create_token_response( + self, request: common.Request, token_handler: Callable + ) -> tuple[dict, str, int]: + """Return token or error in json format. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instance, for example of type + oauthlib.oauth2.BearerToken. + + If the access token request is valid and authorized, the + authorization server issues an access token and optional refresh + token as described in `Section 5.1`_. If the request failed client + authentication or is invalid, the authorization server returns an + error response as described in `Section 5.2`_. + .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 + """ + headers = self._get_default_headers() + try: + if self.request_validator.client_authentication_required( + request + ) and not self.request_validator.authenticate_client(request): + raise rfc6749_errors.InvalidClientError(request=request) + + self.validate_token_request(request) + + except rfc6749_errors.OAuth2Error as e: + headers.update(e.headers) + return headers, e.json, e.status_code + + token = token_handler.create_token(request, self.refresh_token) + + self.request_validator.save_token(token, request) + + return headers, json.dumps(token), 200 diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 7c9393e6..62983014 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -19,8 +19,8 @@ ImplicitGrant as OAuth2ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ) +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant from oauthlib.oauth2.rfc6749.tokens import BearerToken -from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint from ..grant_types import ( AuthorizationCodeGrant, @@ -45,7 +45,10 @@ class Server( RevocationEndpoint, UserInfoEndpoint, ): - """An all-in-one endpoint featuring all four major grant types.""" + """ + An all-in-one endpoint featuring all four major grant types + and extension grants. + """ def __init__( self, @@ -77,6 +80,7 @@ def __init__( self.openid_connect_auth = AuthorizationCodeGrant(request_validator) self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator) + self.device_code_grant = DeviceCodeGrant(request_validator) self.bearer = BearerToken( request_validator, token_generator, token_expires_in, refresh_token_generator @@ -123,6 +127,7 @@ def __init__( "password": self.password_grant, "client_credentials": self.credentials_grant, "refresh_token": self.refresh_grant, + "urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant, }, default_token_type=self.bearer, ) From baf0c0037b1b590b11b5d9c7ee935603bfd0838d Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:43:23 +0000 Subject: [PATCH 03/12] Allow interval to be controlled by caller Any caller of DeviceApplicationServer() needs control of the inerval polling time. This is part of the flow where the device polls the auth server to check in the user has submitted the user_code --- oauthlib/oauth2/rfc8628/endpoints/pre_configured.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index cdb6b313..c516f621 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -2,6 +2,7 @@ DeviceAuthorizationEndpoint, ) from typing import Callable +from oauthlib.openid.connect.core.request_validator import RequestValidator class DeviceApplicationServer(DeviceAuthorizationEndpoint): @@ -9,8 +10,9 @@ class DeviceApplicationServer(DeviceAuthorizationEndpoint): def __init__( self, - request_validator, - verification_uri, + request_validator: RequestValidator, + interval: int, + verification_uri: str, user_code_generator: Callable[[None], str] = None, **kwargs, ): @@ -18,12 +20,14 @@ def __init__( :param request_validator: An implementation of oauthlib.oauth2.rfc8626.RequestValidator. + :param interval: How long the device needs to wait before polling the server :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, + interval=interval, verification_uri=verification_uri, user_code_generator=user_code_generator, ) From 3e25f592c76252326c326bd4d580b12d34cb24f1 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:29:29 +0000 Subject: [PATCH 04/12] Use absolute imports Easier to read, easier to derive where the module lives. --- oauthlib/oauth2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index 2630f694..3bb51021 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -66,5 +66,5 @@ from .rfc6749.tokens import BearerToken, OAuth2Token from .rfc6749.utils import is_secure_transport from .rfc8628.clients import DeviceClient -from .rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer +from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant From 58e766cd078f5bf82af3c3d59574e3bd60487cfc Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:00:44 +0000 Subject: [PATCH 05/12] Add device grant to oauth2 based server This grant type handler exists in both the openid conenct server and the oauth 2.0 server. Why? It sets up for a potentitial open id over device flow implementation. It also alo ensures the access_token and refresh_token still get returned whether openid is enabled or not. device flow isn't a spec that's part of open id but some IDps like microsoft, auth0, okta implement an off-spec version. However, that's not the concern of the pr this commit is apart of but if returning an id_token(openid) is needed down the line that will have to be a seperate issue rasied. --- oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index d64a1663..ee55b10d 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -15,12 +15,16 @@ from .resource import ResourceEndpoint from .revocation import RevocationEndpoint from .token import TokenEndpoint +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint): - """An all-in-one endpoint featuring all four major grant types.""" + """ + An all-in-one endpoint featuring all four major grant types + and extension grants. + """ def __init__(self, request_validator, token_expires_in=None, token_generator=None, refresh_token_generator=None, @@ -44,6 +48,7 @@ def __init__(self, request_validator, token_expires_in=None, request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) + self.device_code_grant = DeviceCodeGrant(request_validator) self.bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) @@ -62,6 +67,7 @@ def __init__(self, request_validator, token_expires_in=None, 'password': self.password_grant, 'client_credentials': self.credentials_grant, 'refresh_token': self.refresh_grant, + "urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant }, default_token_type=self.bearer) ResourceEndpoint.__init__(self, default_token='Bearer', From 2a2b24b8f1ccd1e427434db483559f3f260933f1 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:31:02 +0000 Subject: [PATCH 06/12] Add interval default to align with rfc --- oauthlib/oauth2/rfc8628/endpoints/pre_configured.py | 2 +- tests/openid/connect/core/grant_types/test_device_code.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/openid/connect/core/grant_types/test_device_code.py diff --git a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index c516f621..bfc6c404 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -11,8 +11,8 @@ class DeviceApplicationServer(DeviceAuthorizationEndpoint): def __init__( self, request_validator: RequestValidator, - interval: int, verification_uri: str, + interval: int = 5, user_code_generator: Callable[[None], str] = None, **kwargs, ): diff --git a/tests/openid/connect/core/grant_types/test_device_code.py b/tests/openid/connect/core/grant_types/test_device_code.py new file mode 100644 index 00000000..e69de29b From b8a74e74a58adc1aade0328346511994a3093f62 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:44:28 +0000 Subject: [PATCH 07/12] Add device code grant tests --- .../rfc8628/grant_types/__init__.py} | 0 .../rfc8628/grant_types/test_device_code.py | 173 ++++++++++++++++++ 2 files changed, 173 insertions(+) rename tests/{openid/connect/core/grant_types/test_device_code.py => oauth2/rfc8628/grant_types/__init__.py} (100%) create mode 100644 tests/oauth2/rfc8628/grant_types/test_device_code.py diff --git a/tests/openid/connect/core/grant_types/test_device_code.py b/tests/oauth2/rfc8628/grant_types/__init__.py similarity index 100% rename from tests/openid/connect/core/grant_types/test_device_code.py rename to tests/oauth2/rfc8628/grant_types/__init__.py diff --git a/tests/oauth2/rfc8628/grant_types/test_device_code.py b/tests/oauth2/rfc8628/grant_types/test_device_code.py new file mode 100644 index 00000000..973829d5 --- /dev/null +++ b/tests/oauth2/rfc8628/grant_types/test_device_code.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +import json +from unittest import mock +import pytest + +from oauthlib import common + +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +def create_request(body: str = "") -> common.Request: + request = common.Request("http://a.b/path", body=body or None) + request.scopes = ("hello", "world") + request.expires_in = 1800 + request.client = "batman" + request.client_id = "abcdef" + request.code = "1234" + request.response_type = "code" + request.grant_type = "urn:ietf:params:oauth:grant-type:device_code" + request.redirect_uri = "https://a.b/" + return request + + +def create_device_code_grant(mock_validator: mock.MagicMock) -> DeviceCodeGrant: + return DeviceCodeGrant(request_validator=mock_validator) + + +def test_custom_auth_validators_unsupported(): + custom_validator = mock.Mock() + validator = mock.MagicMock() + + expected = ( + "DeviceCodeGrant does not " + "support authorization validators. Use token validators instead." + ) + with pytest.raises(ValueError, match=expected): + DeviceCodeGrant(validator, pre_auth=[custom_validator]) + + with pytest.raises(ValueError, match=expected): + DeviceCodeGrant(validator, post_auth=[custom_validator]) + + expected = "'tuple' object has no attribute 'append'" + auth = DeviceCodeGrant(validator) + with pytest.raises(AttributeError, match=expected): + auth.custom_validators.pre_auth.append(custom_validator) + + +def test_custom_pre_and_post_token_validators(): + client = mock.MagicMock() + + validator = mock.MagicMock() + pre_token_validator = mock.Mock() + post_token_validator = mock.Mock() + + request: common.Request = create_request() + request.client = client + + auth = DeviceCodeGrant(validator) + + auth.custom_validators.pre_token.append(pre_token_validator) + auth.custom_validators.post_token.append(post_token_validator) + + bearer = BearerToken(validator) + auth.create_token_response(request, bearer) + + pre_token_validator.assert_called() + post_token_validator.assert_called() + + +def test_create_token_response(): + validator = mock.MagicMock() + request: common.Request = create_request() + request.client = mock.Mock() + + auth = DeviceCodeGrant(validator) + + bearer = BearerToken(validator) + + headers, body, status_code = auth.create_token_response(request, bearer) + token = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + } + + # when a custom token generator callable isn't used + # the random generator is used as default for the access token + assert token == { + "access_token": mock.ANY, + "expires_in": 3600, + "token_type": "Bearer", + "scope": "hello world", + "refresh_token": mock.ANY, + } + + assert status_code == 200 + + validator.save_token.assert_called_once() + + +def test_invalid_client_error(): + validator = mock.MagicMock() + request: common.Request = create_request() + request.client = mock.Mock() + + auth = DeviceCodeGrant(validator) + bearer = BearerToken(validator) + + validator.authenticate_client.return_value = False + + headers, body, status_code = auth.create_token_response(request, bearer) + body = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + "WWW-Authenticate": 'Bearer error="invalid_client"', + } + assert body == {"error": "invalid_client"} + assert status_code == 401 + + validator.save_token.assert_not_called() + + +def test_invalid_grant_type_error(): + validator = mock.MagicMock() + request: common.Request = create_request() + request.client = mock.Mock() + + request.grant_type = "not_device_code" + + auth = DeviceCodeGrant(validator) + bearer = BearerToken(validator) + + headers, body, status_code = auth.create_token_response(request, bearer) + body = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + } + assert body == {"error": "unsupported_grant_type"} + assert status_code == 400 + + validator.save_token.assert_not_called() + + +def test_duplicate_params_error(): + validator = mock.MagicMock() + request: common.Request = create_request( + "client_id=123&scope=openid&scope=openid" + ) + request.client = mock.Mock() + + auth = DeviceCodeGrant(validator) + bearer = BearerToken(validator) + + headers, body, status_code = auth.create_token_response(request, bearer) + body = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + } + assert body == {"error": "invalid_request", "error_description": "Duplicate scope parameter."} + assert status_code == 400 + + validator.save_token.assert_not_called() From 2ade8707a8ddeee5408fc07541b3b1d9bc5c6c28 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:57:27 +0000 Subject: [PATCH 08/12] Add device code grant docs --- docs/oauth2/grants/device_code.rst | 6 ++++++ docs/oauth2/grants/grants.rst | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/oauth2/grants/device_code.rst diff --git a/docs/oauth2/grants/device_code.rst b/docs/oauth2/grants/device_code.rst new file mode 100644 index 00000000..58ce6698 --- /dev/null +++ b/docs/oauth2/grants/device_code.rst @@ -0,0 +1,6 @@ +Device code Grant +----------------- + +.. autoclass:: oauthlib.oauth2.DeviceCodeGrant + :members: + :inherited-members: diff --git a/docs/oauth2/grants/grants.rst b/docs/oauth2/grants/grants.rst index e1837617..d877bac6 100644 --- a/docs/oauth2/grants/grants.rst +++ b/docs/oauth2/grants/grants.rst @@ -10,6 +10,7 @@ Grant types password credentials refresh + device_code jwt custom_validators custom_grant @@ -26,6 +27,13 @@ degree of trust between the resource owner and the client, and when other authorization grant types are not available. This is also often used for legacy applications to incrementally transition to OAuth 2. +The device code grant(officially referred to as 'urn:ietf:params:oauth:grant-type:device_code') +is used when trying to authenticate device with limited or no input capabilities by getting +the user to approve the login on an external device (like a mobile phone or laptop) in their +possession that they're already logged into. Unlike the previously mentioned grants it is an extension grant, which is a type of grant +to address specific authorization scenarios. +:doc:`Device code grant ` + The main purpose of the grant types is to authorize access to protected resources in various ways with different security credentials. From 92f62ebf77e66de22dc8ee346e99a5f2a0d9b292 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:59:55 +0000 Subject: [PATCH 09/12] Apply ruff format using line-length 99 The 255 is too high, leads to lines that are too long. I'll get a pr to format the entire repo after this one --- .../rfc6749/endpoints/pre_configured.py | 269 +++++++++++------- oauthlib/oauth2/rfc8628/__init__.py | 6 +- oauthlib/oauth2/rfc8628/errors.py | 15 +- 3 files changed, 183 insertions(+), 107 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index ee55b10d..ef7db0c9 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -5,9 +5,13 @@ This module is an implementation of various endpoints needed for providing OAuth 2.0 RFC6749 servers. """ + from ..grant_types import ( - AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, + AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant, ) from ..tokens import BearerToken from .authorization import AuthorizationEndpoint @@ -18,17 +22,23 @@ from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant -class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint): - +class Server( + AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint +): """ An all-in-one endpoint featuring all four major grant types and extension grants. """ - 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 @@ -44,45 +54,58 @@ def __init__(self, request_validator, token_expires_in=None, """ self.auth_grant = AuthorizationCodeGrant(request_validator) self.implicit_grant = ImplicitGrant(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.device_code_grant = DeviceCodeGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={ - 'code': self.auth_grant, - 'token': self.implicit_grant, - 'none': self.auth_grant - }, - default_token_type=self.bearer) - - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': self.auth_grant, - 'password': self.password_grant, - 'client_credentials': self.credentials_grant, - 'refresh_token': self.refresh_grant, - "urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + + AuthorizationEndpoint.__init__( + self, + default_response_type="code", + response_types={ + "code": self.auth_grant, + "token": self.implicit_grant, + "none": self.auth_grant, + }, + default_token_type=self.bearer, + ) + + TokenEndpoint.__init__( + self, + default_grant_type="authorization_code", + grant_types={ + "authorization_code": self.auth_grant, + "password": self.password_grant, + "client_credentials": self.credentials_grant, + "refresh_token": self.refresh_grant, + "urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) -class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint): - +class WebApplicationServer( + AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint +): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, refresh_token_generator=None, **kwargs): + def __init__( + self, + request_validator, + token_generator=None, + token_expires_in=None, + refresh_token_generator=None, + **kwargs, + ): """Construct a new web application server. :param request_validator: An implementation of @@ -98,30 +121,44 @@ def __init__(self, request_validator, token_generator=None, """ self.auth_grant = AuthorizationCodeGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={'code': self.auth_grant}, - default_token_type=self.bearer) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': self.auth_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + AuthorizationEndpoint.__init__( + self, + default_response_type="code", + response_types={"code": self.auth_grant}, + default_token_type=self.bearer, + ) + TokenEndpoint.__init__( + self, + default_grant_type="authorization_code", + grant_types={ + "authorization_code": self.auth_grant, + "refresh_token": self.refresh_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) -class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, - ResourceEndpoint, RevocationEndpoint): - +class MobileApplicationServer( + AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint +): """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, refresh_token_generator=None, **kwargs): + def __init__( + self, + request_validator, + token_generator=None, + token_expires_in=None, + refresh_token_generator=None, + **kwargs, + ): """Construct a new implicit grant server. :param request_validator: An implementation of @@ -136,27 +173,39 @@ def __init__(self, request_validator, token_generator=None, token-, resource-, and revocation-endpoint constructors. """ self.implicit_grant = ImplicitGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - AuthorizationEndpoint.__init__(self, default_response_type='token', - response_types={ - 'token': self.implicit_grant}, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) - RevocationEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) - - -class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, - ResourceEndpoint, RevocationEndpoint): + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + AuthorizationEndpoint.__init__( + self, + default_response_type="token", + response_types={"token": self.implicit_grant}, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) + RevocationEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) + IntrospectEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) + +class LegacyApplicationServer( + TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint +): """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, refresh_token_generator=None, **kwargs): + def __init__( + self, + request_validator, + token_generator=None, + token_expires_in=None, + refresh_token_generator=None, + **kwargs, + ): """Construct a resource owner password credentials grant server. :param request_validator: An implementation of @@ -170,30 +219,40 @@ def __init__(self, request_validator, token_generator=None, :param kwargs: Extra parameters to pass to authorization-, token-, resource-, and revocation-endpoint constructors. """ - self.password_grant = ResourceOwnerPasswordCredentialsGrant( - request_validator) + self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - TokenEndpoint.__init__(self, default_grant_type='password', - grant_types={ - 'password': self.password_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + TokenEndpoint.__init__( + self, + default_grant_type="password", + grant_types={ + "password": self.password_grant, + "refresh_token": self.refresh_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) -class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, - ResourceEndpoint, RevocationEndpoint): - +class BackendApplicationServer( + TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint +): """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, refresh_token_generator=None, **kwargs): + def __init__( + self, + request_validator, + token_generator=None, + token_expires_in=None, + refresh_token_generator=None, + **kwargs, + ): """Construct a client credentials grant server. :param request_validator: An implementation of @@ -208,15 +267,21 @@ def __init__(self, request_validator, token_generator=None, token-, resource-, and revocation-endpoint constructors. """ self.credentials_grant = ClientCredentialsGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - TokenEndpoint.__init__(self, default_grant_type='client_credentials', - grant_types={ - 'client_credentials': self.credentials_grant}, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) - RevocationEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + TokenEndpoint.__init__( + self, + default_grant_type="client_credentials", + grant_types={"client_credentials": self.credentials_grant}, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) + RevocationEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) + IntrospectEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index f152d41a..65891445 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -6,7 +6,11 @@ for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ -from oauthlib.oauth2.rfc8628.errors import SlowDownError, AuthorizationPendingError, ExpiredTokenError +from oauthlib.oauth2.rfc8628.errors import ( + SlowDownError, + AuthorizationPendingError, + ExpiredTokenError, +) import logging log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc8628/errors.py b/oauthlib/oauth2/rfc8628/errors.py index 0bc0feb2..a4359389 100644 --- a/oauthlib/oauth2/rfc8628/errors.py +++ b/oauthlib/oauth2/rfc8628/errors.py @@ -8,6 +8,7 @@ defined error responses specific to the the device grant """ + class AuthorizationPendingError(OAuth2Error): """ For the device authorization grant; @@ -21,7 +22,8 @@ class AuthorizationPendingError(OAuth2Error): increase in the polling interval required by the "slow_down" error. """ - error = 'authorization_pending' + + error = "authorization_pending" class SlowDownError(OAuth2Error): @@ -30,7 +32,9 @@ class SlowDownError(OAuth2Error): still pending and polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests. """ - error = 'slow_down' + + error = "slow_down" + class ExpiredTokenError(OAuth2Error): """ @@ -39,10 +43,13 @@ class ExpiredTokenError(OAuth2Error): authorization request but SHOULD wait for user interaction before restarting to avoid unnecessary polling. """ - error = 'expired_token' + + error = "expired_token" + class AccessDenied(OAuth2Error): """ The authorization request was denied. """ - error = 'access_denied' + + error = "access_denied" From 27396f0f9af8602afc7feddf6436041a4cf92d75 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Sat, 16 Nov 2024 16:16:09 +0000 Subject: [PATCH 10/12] Add device code grant to support grant claiims --- tests/oauth2/rfc6749/endpoints/test_metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index bcb9c0f5..facf69d0 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -98,6 +98,7 @@ def test_server_metadata(self): "scopes_supported": ["email", "profile"], "grant_types_supported": [ "authorization_code", + "urn:ietf:params:oauth:grant-type:device_code", "password", "client_credentials", "refresh_token", From f5f6f90585e988883d6201e3d69420a597a1b277 Mon Sep 17 00:00:00 2001 From: David Uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:27:04 +0000 Subject: [PATCH 11/12] Add pseudocode example of how to implement the device flow --- docs/oauth2/grants/device_code.rst | 6 + examples/device_code_flow.py | 260 +++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 examples/device_code_flow.py diff --git a/docs/oauth2/grants/device_code.rst b/docs/oauth2/grants/device_code.rst index 58ce6698..ff372b5c 100644 --- a/docs/oauth2/grants/device_code.rst +++ b/docs/oauth2/grants/device_code.rst @@ -4,3 +4,9 @@ Device code Grant .. autoclass:: oauthlib.oauth2.DeviceCodeGrant :members: :inherited-members: + + +An pseudocode/skeleton example of how the device flow can be implemented is +available in the `examples`_ folder on GitHub. + +.. _`examples`: https://github.com/oauthlib/oauthlib/blob/master/examples/device_code_flow.py diff --git a/examples/device_code_flow.py b/examples/device_code_flow.py new file mode 100644 index 00000000..d281491f --- /dev/null +++ b/examples/device_code_flow.py @@ -0,0 +1,260 @@ +import enum +import json +import datetime +from datetime import timedelta + +from oauthlib.oauth2 import RequestValidator, Server, DeviceApplicationServer +from oauthlib.oauth2.rfc8628 import errors as device_flow_errors +from oauthlib.oauth2.rfc8628.errors import AccessDenied, AuthorizationPendingError, ExpiredTokenError, SlowDownError + + +""" +A pseudocode implementation of the device flow code under an Oauth2 provider. + +This example is not concerned with openid in any way. + +This example is also not a 1:1 pseudocode implementation. Please refer to the rfc +for the full details. +https://datatracker.ietf.org/doc/html/rfc8628 + +This module is just acting as a way to demonstrate the main pieces +needed in oauthlib to implement the flow + + +We also assume you already have the /token & /login endpoint in your provider. + +Your provider will also need the following endpoints(which will be discussed +in the example below): + - /device_authorization (part of rfc) + - /device (part of rfc) + - /approve-deny (up to your implementation, this is an example) +""" + + +""" +Device flow pseudocode implementation step by step: + 0. Providing some way to represent the device flow session + + Some python object to represent the current state of the device during + the device flow. This, for example, could be an object that persists + and represents the device in a database +""" + + +class Device: + class DeviceFlowStatus(enum.Enum): + AUTHORIZED = "Authorized" + AUTHORIZATION_PENDING = "Authorization_pending" + EXPIRED = "Expired" + DENIED = "Denied" + + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 + id = ... # if Device is representing a database object, this will be the id of that row + device_code = ... + user_code = ... + scope = ... + interval = ... # in seconds, default is 5 + expires = ... # seconds + status = ... # DeviceFlowStatus with AUTHORIZATION_PENDING as the default + + client_id = ... + last_checked = ... # datetime + + +""" + 1. User goes on their device(client) and the client sends a request to /device_authorization + against the provider: + https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 + https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + + + POST /device_authorization HTTP/1.1 + Host: server.example.com + Content-Type: application/x-www-form-urlencoded + + client_id=1406020730&scope=example_scope + + Response: + 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 + } +""" + + +class DeviceAuthorizationEndpoint: + @staticmethod + def create_device_authorization_response(request): + server = DeviceApplicationServer(interval=5, verification_uri="https://example.com/device") + return server.create_device_authorization_response(request) + + def post(self, request): + headers, data, status = self.create_device_authorization_response(request) + device_response = ... + + # Create an instance of examples.device_flow.Device` using `request` and `data`that encapsulates + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 & + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + + return device_response + + +""" + 2. Client presents the information to the user + (There's a section on non visual capable devices as well + https://datatracker.ietf.org/doc/html/rfc8628#section-5.7) + +-------------------------------------------------+ + | | + | Scan the QR code or, using +------------+ | + | a browser on another device, |[_].. . [_]| | + | visit: | . .. . .| | + | https://example.com/device | . . . ....| | + | |. . . . | | + | And enter the code: |[_]. ... . | | + | WDJB-MJHT +------------+ | + | | + +-------------------------------------------------+ +""" +# The implementation for step 2 is up to the owner of device. + + +"""" + 3 (The browser flow). User goes to https://example.com/device where they're presented with a + form to fill in the user code. + + Implement that endpoint on your provider and follow the logic in the rfc. + + Making use of the errors in `oauthlib.oauth2.rfc8628.errors` + + raise AccessDenied/AuthorizationPendingError/ExpiredTokenError where appropriate making use of + `examples.device_flow.Device` to get and update current state of the device during the session + + If the user isn't logged in(after inputting the user-code), they should be redirected to the provider's /login + endpoint and redirected back to an /approve-deny endpoint(The name and implementation of /approve-deny is up + to the owner of the provider, this is just an example). + They should then see an "approve" or "deny" button to authorize the device. + + Again, using `examples.device_flow.Device` to update the status appropriately during the session. +""" +# /device and /approve-deny is up to the owner of the provider to implement. Again, make sure to +# keep referring to the rfc when implementing. + + +""" +4 (The polling flow) + https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 + https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 + + + Right after step 2, the device polls the /token endpoint every "interval" amount of seconds + to check if user has approved or denied the request. + + When grant type is `urn:ietf:params:oauth:grant-type:device_code`, + `oauthlib.oauth2.rfc8628.grant_types.device_code.DeviceCodeGrant` will be the handler + that handles token generation. +""" + + +# This is purely for illustrative purposes +# to demonstrate rate limiting on the token endpoint for the device flow. +# It is up to as the provider to decide how you want +# to rate limit the device during polling. +def rate_limit(func, rate="1/5s"): + def wrapper(): + # some logic to ensure client device is rate limited by a minimum + # of 1 request every 5 seconds during device polling + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + + # use device_code to retrieve device + device = Device + + # get the time in seconds since the device polled the /token endpoint + now = datetime.datetime.now(tz=datetime.UTC) + diff = now - timedelta(device.last_checked) + total_seconds_since_last_device_poll = diff.total_seconds() + + device.last_checked = now + + # for illustrative purposes. 1/5s means 1 request every 5 seconds. + # so if `total_seconds_since_last_device_poll` is 4 seconds, this will + # raise an error + if total_seconds_since_last_device_poll < rate: + raise device_flow_errors.SlowDownError() + + result = func() + return result + + return wrapper + + +class ExampleRequestValidator(RequestValidator): + # All the other methods that need to be implemented... + # see examples.skeleton_oauth2_web_application_server.SkeletonValidator + # for a more complete example. + + # Here our main concern is this method: + def create_token_response(self): ... + + +class ServerSetupForTokenEndpoint: + def __init__(self): + validator = ExampleRequestValidator + self.server = Server(validator) + + +# You should already have the /token endpoint implemented in your provider. +class TokenEndpoint(ServerSetupForTokenEndpoint): + def default_flow_token_response(self, request): + url, headers, body, status = self.server.create_token_response(request) + access_token = json.loads(body).get("access_token") + + # return access_token in a http response + return access_token + + @rate_limit # this will raise the SlowDownError + def device_flow_token_response(self, request, device_code): + """ + Following the rfc, this will route the device request accordingly and raise + required errors. + + Remember that unlike other auth flows, the device if polling this endpoint once + every "interval" amount of seconds. + """ + # using device_code arg to retrieve the correct device object instance + device = Device + + if device.status == device.DeviceFlowStatus.AUTHORIZATION_PENDING: + raise AuthorizationPendingError() + + # If user clicked "deny" in the /approve-deny page endpoint. + # the device gets set to 'authorized' in /approve-deny and /device checks + # if someone tries to input a code for a user code that's already been authorized + if device.status == device.DeviceFlowStatus.DENIED: + raise AccessDenied() + + url, headers, body, status = self.server.create_token_response(request) + + access_token = json.loads(body).get("access_token") + + device.status = device.EXPIRED + + # return access_token in a http response + return access_token + + # Example of how token endpoint could handle the token creation depending on + # the grant type during a POST to /token. + def post(self, request): + params = request.POST + if params.get("grant_type") == "urn:ietf:params:oauth:grant-type:device_code": + return self.device_flow_token_response(request, params["device_code"]) + return self.default_flow_token_response(request) From b9702481f92d1e8382de3456a2df3da18a171c9d Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sat, 7 Dec 2024 21:50:51 +0600 Subject: [PATCH 12/12] Update tests/oauth2/rfc8628/grant_types/test_device_code.py --- tests/oauth2/rfc8628/grant_types/test_device_code.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/oauth2/rfc8628/grant_types/test_device_code.py b/tests/oauth2/rfc8628/grant_types/test_device_code.py index 973829d5..da0592f7 100644 --- a/tests/oauth2/rfc8628/grant_types/test_device_code.py +++ b/tests/oauth2/rfc8628/grant_types/test_device_code.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import json from unittest import mock import pytest 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