This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
"""
from oauthlib.oauth2.rfc8628.errors import (
SlowDownError,
AuthorizationPendingError,
ExpiredTokenError,
)
import logging
log = logging.getLogger(__name__)

View File

@@ -0,0 +1,8 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming OAuth 2.0 Device Authorization RFC8628.
"""
from .device import DeviceClient

View File

@@ -0,0 +1,95 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
"""
from oauthlib.common import add_params_to_uri
from oauthlib.oauth2 import BackendApplicationClient, Client
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
class DeviceClient(Client):
"""A public client utilizing the device authorization workflow.
The client can request an access token using a device code and
a public client id associated with the device code as defined
in RFC8628.
The device authorization grant type can be used to obtain both
access tokens and refresh tokens and is intended to be used in
a scenario where the device being authorized does not have a
user interface that is suitable for performing authentication.
"""
grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
def __init__(self, client_id, **kwargs):
super().__init__(client_id, **kwargs)
self.client_secret = kwargs.get('client_secret')
def prepare_request_uri(self, uri, scope=None, **kwargs):
if not is_secure_transport(uri):
raise InsecureTransportError()
scope = self.scope if scope is None else scope
params = [(('client_id', self.client_id)), (('grant_type', self.grant_type))]
if self.client_secret is not None:
params.append(('client_secret', self.client_secret))
if scope:
params.append(('scope', list_to_scope(scope)))
for k,v in kwargs.items():
if v:
params.append((str(k), v))
return add_params_to_uri(uri, params)
def prepare_request_body(self, device_code, body='', scope=None,
include_client_id=False, **kwargs):
"""Add device_code to request body
The client makes a request to the token endpoint by adding the
device_code as a parameter using the
"application/x-www-form-urlencoded" format to the HTTP request
body.
:param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra parameters. Default ''.
:param scope: The scope of the access request as described by
`Section 3.3`_.
:param include_client_id: `True` to send the `client_id` in the
body of the upstream request. This is required
if the client is not authenticating with the
authorization server as described in
`Section 3.2.1`_. False otherwise (default).
:type include_client_id: Boolean
:param kwargs: Extra credentials to include in the token request.
The prepared body will include all provided device_code as well as
the ``grant_type`` parameter set to
``urn:ietf:params:oauth:grant-type:device_code``::
>>> from oauthlib.oauth2 import DeviceClient
>>> client = DeviceClient('your_id', 'your_code')
>>> client.prepare_request_body(scope=['hello', 'world'])
'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
.. _`Section 3.2.1`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1
.. _`Section 3.3`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
.. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
"""
kwargs['client_id'] = self.client_id
kwargs['include_client_id'] = include_client_id
scope = self.scope if scope is None else scope
return prepare_token_request(self.grant_type, body=body, device_code=device_code,
scope=scope, **kwargs)

View File

@@ -0,0 +1,10 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
"""
from .device_authorization import DeviceAuthorizationEndpoint
from .pre_configured import DeviceApplicationServer

View File

@@ -0,0 +1,232 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC8628.
"""
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,
)
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
the device authorization endpoint.
The client authentication requirements of Section 3.2.1 of [RFC6749]
apply to requests on this endpoint, which means that confidential
clients (those that have established client credentials) authenticate
in the same manner as when making requests to the token endpoint, and
public clients provide the "client_id" parameter to identify
themselves.
"""
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.
:param verification_uri: a string containing the URL that can be polled by the client application
: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.user_code_generator = user_code_generator
BaseEndpoint.__init__(self)
@property
def interval(self):
"""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.
"""
return self._interval
@property
def expires_in(self):
"""The lifetime in seconds of the "device_code" and "user_code"."""
return self._expires_in
@property
def verification_uri(self):
"""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.
"""
return self._verification_uri
def verification_uri_complete(self, user_code):
if not self._verification_uri_complete:
return None
if isinstance(self._verification_uri_complete, str):
return self._verification_uri_complete.format(user_code=user_code)
if callable(self._verification_uri_complete):
return self._verification_uri_complete(user_code)
return None
@catch_errors_and_unavailability
def validate_device_authorization_request(self, request):
"""Validate the device authorization request.
The client_id is required if the client is not authenticating with the
authorization server as described in `Section 3.2.1. of [RFC6749]`_.
The client identifier as described in `Section 2.2 of [RFC6749]`_.
.. _`Section 3.2.1. of [RFC6749]`: https://www.rfc-editor.org/rfc/rfc6749#section-3.2.1
.. _`Section 2.2 of [RFC6749]`: https://www.rfc-editor.org/rfc/rfc6749#section-2.2
"""
# First check duplicate parameters
for param in ("client_id", "scope"):
try:
duplicate_params = request.duplicate_params
except ValueError:
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
)
# the "application/x-www-form-urlencoded" format, per Appendix B of [RFC6749]
# https://www.rfc-editor.org/rfc/rfc6749#appendix-B
if request.headers["Content-Type"] != "application/x-www-form-urlencoded":
raise errors.InvalidRequestError(
"Content-Type must be application/x-www-form-urlencoded",
request=request,
)
# REQUIRED. The client identifier as described in Section 2.2.
# https://tools.ietf.org/html/rfc6749#section-2.2
# TODO: extract client_id an helper validation function.
if not request.client_id:
raise errors.MissingClientIdError(request=request)
if not self.request_validator.validate_client_id(request.client_id, request):
raise errors.InvalidClientIdError(request=request)
# The client authentication requirements of Section 3.2.1 of [RFC6749]
# apply to requests on this endpoint, which means that confidential
# clients (those that have established client credentials) authenticate
# in the same manner as when making requests to the token endpoint, and
# public clients provide the "client_id" parameter to identify
# themselves.
self._raise_on_invalid_client(request)
@catch_errors_and_unavailability
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)
headers = {}
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,
"user_code": user_code,
"device_code": generate_token(),
}
if self.interval is not None:
data["interval"] = self.interval
verification_uri_complete = self.verification_uri_complete(user_code)
if verification_uri_complete:
data["verification_uri_complete"] = verification_uri_complete
return headers, data, 200

View File

@@ -0,0 +1,36 @@
from oauthlib.oauth2.rfc8628.endpoints.device_authorization import (
DeviceAuthorizationEndpoint,
)
from typing import Callable, Optional
from oauthlib.openid.connect.core.request_validator import RequestValidator
class DeviceApplicationServer(DeviceAuthorizationEndpoint):
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
def __init__(
self,
request_validator: RequestValidator,
verification_uri: str,
interval: int = 5,
verification_uri_complete: Optional[str] = None, # noqa: FA100
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 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,
verification_uri_complete=verification_uri_complete,
)

View File

@@ -0,0 +1,55 @@
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"

View File

@@ -0,0 +1 @@
from oauthlib.oauth2.rfc8628.grant_types.device_code import DeviceCodeGrant

View File

@@ -0,0 +1,111 @@
from __future__ import annotations
import json
from typing import Callable
from oauthlib import common # noqa: TC001
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

View File

@@ -0,0 +1,25 @@
from oauthlib.oauth2 import RequestValidator as OAuth2RequestValidator
class RequestValidator(OAuth2RequestValidator):
def client_authentication_required(self, request, *args, **kwargs):
"""Determine if client authentication is required for current request.
According to the rfc8628, client authentication is required in the following cases:
- Device Authorization Request follows the, the client authentication requirements
of Section 3.2.1 of [RFC6749] apply to requests on this endpoint, which means that
confidential clients (those that have established client credentials) authenticate
in the same manner as when making requests to the token endpoint, and
public clients provide the "client_id" parameter to identify themselves,
see `Section 3.1`_.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
- Device Authorization Request
.. _`Section 3.1`: https://www.rfc-editor.org/rfc/rfc8628#section-3.1
"""
return True