summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/oauth2/clients/deviceclient.rst5
-rw-r--r--oauthlib/oauth2/__init__.py1
-rw-r--r--oauthlib/oauth2/rfc8628/__init__.py10
-rw-r--r--oauthlib/oauth2/rfc8628/clients/__init__.py8
-rw-r--r--oauthlib/oauth2/rfc8628/clients/device.py94
-rw-r--r--tests/oauth2/rfc8628/__init__.py0
-rw-r--r--tests/oauth2/rfc8628/clients/__init__.py0
-rw-r--r--tests/oauth2/rfc8628/clients/test_device.py63
8 files changed, 181 insertions, 0 deletions
diff --git a/docs/oauth2/clients/deviceclient.rst b/docs/oauth2/clients/deviceclient.rst
new file mode 100644
index 0000000..d4e8d7d
--- /dev/null
+++ b/docs/oauth2/clients/deviceclient.rst
@@ -0,0 +1,5 @@
+DeviceClient
+------------------------
+
+.. autoclass:: oauthlib.oauth2.DeviceClient
+ :members:
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py
index a6e1ccc..deefb1a 100644
--- a/oauthlib/oauth2/__init__.py
+++ b/oauthlib/oauth2/__init__.py
@@ -33,3 +33,4 @@ from .rfc6749.grant_types import (
from .rfc6749.request_validator import RequestValidator
from .rfc6749.tokens import BearerToken, OAuth2Token
from .rfc6749.utils import is_secure_transport
+from .rfc8628.clients import DeviceClient
diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py
new file mode 100644
index 0000000..531929d
--- /dev/null
+++ b/oauthlib/oauth2/rfc8628/__init__.py
@@ -0,0 +1,10 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 Device Authorization RFC8628.
+"""
+import logging
+
+log = logging.getLogger(__name__)
diff --git a/oauthlib/oauth2/rfc8628/clients/__init__.py b/oauthlib/oauth2/rfc8628/clients/__init__.py
new file mode 100644
index 0000000..130b52e
--- /dev/null
+++ b/oauthlib/oauth2/rfc8628/clients/__init__.py
@@ -0,0 +1,8 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 Device Authorization RFC8628.
+"""
+from .device import DeviceClient
diff --git a/oauthlib/oauth2/rfc8628/clients/device.py b/oauthlib/oauth2/rfc8628/clients/device.py
new file mode 100644
index 0000000..df7ff68
--- /dev/null
+++ b/oauthlib/oauth2/rfc8628/clients/device.py
@@ -0,0 +1,94 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 Device Authorization RFC8628.
+"""
+
+from oauthlib.oauth2 import BackendApplicationClient, Client
+from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
+from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
+from oauthlib.common import add_params_to_uri
+
+
+class DeviceClient(Client):
+
+ """A public client utilizing the device authorization workflow.
+
+ The client can request an access token using a device code and
+ a public client id associated with the device code as defined
+ in RFC8628.
+
+ The device authorization grant type can be used to obtain both
+ access tokens and refresh tokens and is intended to be used in
+ a scenario where the device being authorized does not have a
+ user interface that is suitable for performing authentication.
+ """
+
+ grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
+
+ def __init__(self, client_id, **kwargs):
+ super().__init__(client_id, **kwargs)
+ self.client_secret = kwargs.get('client_secret')
+
+ def prepare_request_uri(self, uri, scope=None, **kwargs):
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ scope = self.scope if scope is None else scope
+ params = [(('client_id', self.client_id)), (('grant_type', self.grant_type))]
+
+ if self.client_secret is not None:
+ params.append(('client_secret', self.client_secret))
+
+ if scope:
+ params.append(('scope', list_to_scope(scope)))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((str(k), kwargs[k]))
+
+ return add_params_to_uri(uri, params)
+
+ def prepare_request_body(self, device_code, body='', scope=None,
+ include_client_id=False, **kwargs):
+ """Add device_code to request body
+
+ The client makes a request to the token endpoint by adding the
+ device_code as a parameter using the
+ "application/x-www-form-urlencoded" format to the HTTP request
+ body.
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+
+ :param include_client_id: `True` to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_. False otherwise (default).
+ :type include_client_id: Boolean
+
+ :param kwargs: Extra credentials to include in the token request.
+
+ The prepared body will include all provided device_code as well as
+ the ``grant_type`` parameter set to
+ ``urn:ietf:params:oauth:grant-type:device_code``::
+
+ >>> from oauthlib.oauth2 import BackendApplicationClient
+ >>> client = DeviceClient('your_id', 'your_code')
+ >>> client.prepare_request_body(scope=['hello', 'world'])
+ 'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
+
+ .. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
+ """
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.grant_type, body=body, device_code=device_code,
+ scope=scope, **kwargs)
diff --git a/tests/oauth2/rfc8628/__init__.py b/tests/oauth2/rfc8628/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/oauth2/rfc8628/__init__.py
diff --git a/tests/oauth2/rfc8628/clients/__init__.py b/tests/oauth2/rfc8628/clients/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/oauth2/rfc8628/clients/__init__.py
diff --git a/tests/oauth2/rfc8628/clients/test_device.py b/tests/oauth2/rfc8628/clients/test_device.py
new file mode 100644
index 0000000..725dea2
--- /dev/null
+++ b/tests/oauth2/rfc8628/clients/test_device.py
@@ -0,0 +1,63 @@
+import os
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2 import DeviceClient
+
+from tests.unittest import TestCase
+
+
+class DeviceClientTest(TestCase):
+
+ client_id = "someclientid"
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+
+ client_secret = "asecret"
+
+ device_code = "somedevicecode"
+
+ scope = ["profile", "email"]
+
+ body = "not=empty"
+
+ body_up = "not=empty&grant_type=urn:ietf:params:oauth:grant-type:device_code"
+ body_code = body_up + "&device_code=somedevicecode"
+ body_kwargs = body_code + "&some=providers&require=extra+arguments"
+
+ uri = "https://example.com/path?query=world"
+ uri_id = uri + "&client_id=" + client_id
+ uri_grant = uri_id + "&grant_type=urn:ietf:params:oauth:grant-type:device_code"
+ uri_secret = uri_grant + "&client_secret=asecret"
+ uri_scope = uri_secret + "&scope=profile+email"
+
+ def test_request_body(self):
+ client = DeviceClient(self.client_id)
+
+ # Basic, no extra arguments
+ body = client.prepare_request_body(self.device_code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ rclient = DeviceClient(self.client_id)
+ body = rclient.prepare_request_body(self.device_code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ # With extra parameters
+ body = client.prepare_request_body(
+ self.device_code, body=self.body, **self.kwargs)
+ self.assertFormBodyEqual(body, self.body_kwargs)
+
+ def test_request_uri(self):
+ client = DeviceClient(self.client_id)
+
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_grant)
+
+ client = DeviceClient(self.client_id, client_secret=self.client_secret)
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_secret)
+
+ uri = client.prepare_request_uri(self.uri, scope=self.scope)
+ self.assertURLEqual(uri, self.uri_scope)