Skip to content

Commit 142a4ae

Browse files
committed
Add pseudocode example of how to implement the device flow
1 parent 27396f0 commit 142a4ae

File tree

2 files changed

+249
-0
lines changed

2 files changed

+249
-0
lines changed

docs/oauth2/grants/device_code.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ Device code Grant
44
.. autoclass:: oauthlib.oauth2.DeviceCodeGrant
55
:members:
66
:inherited-members:
7+
8+
9+
An pseudocode/skeleton example of how the device flow can be implemented is
10+
available in the `examples`_ folder on GitHub.
11+
12+
.. _`examples`: https://github.com/oauthlib/oauthlib/blob/master/examples/device_code_flow.py

examples/device_code_flow.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import enum
2+
import json
3+
4+
5+
from oauthlib.oauth2 import RequestValidator, Server, DeviceApplicationServer
6+
from oauthlib.oauth2.rfc8628 import errors as device_flow_errors
7+
from oauthlib.oauth2.rfc8628.errors import AccessDenied, AuthorizationPendingError, ExpiredTokenError, SlowDownError
8+
9+
10+
"""
11+
A pseudocode implementation of the device flow code under an Oauth2 provider.
12+
13+
This example is not concerned with openid in any way.
14+
15+
This example is also not a 1:1 pseudocode implementation. Please refer to the rfc
16+
for the full details.
17+
https://datatracker.ietf.org/doc/html/rfc8628
18+
19+
This module is just acting as a way to demonstrate the main pieces
20+
needed in oauthlib to implement the flow
21+
22+
23+
We also assume you already have the /token & /login endpoint in your provider.
24+
25+
Your provider will also need the following endpoints(which will be discussed
26+
in the example below):
27+
- /device_authorization (part of rfc)
28+
- /device (part of rfc)
29+
- /approve-deny (up to your implementation, this is an example)
30+
"""
31+
32+
33+
"""
34+
Device flow pseudocode implementation step by step:
35+
0. Providing some way to represent the device flow session
36+
37+
Some python object to represent the current state of the device during
38+
the device flow. This, for example, could be an object that persists
39+
and represents the device in a database
40+
"""
41+
42+
43+
class Device:
44+
class DeviceFlowStatus(enum.Enum):
45+
AUTHORIZED = "Authorized"
46+
AUTHORIZATION_PENDING = "Authorization_pending"
47+
EXPIRED = "Expired"
48+
DENIED = "Denied"
49+
50+
# https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
51+
# https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
52+
id = ...
53+
device_code = ...
54+
user_code = ...
55+
scope = ...
56+
interval = ... # # seconds
57+
expires = ... # seconds
58+
status = ... # DeviceFlowStatus with AUTHORIZATION_PENDING as the default
59+
60+
client_id = ...
61+
last_checked = ... # datetime
62+
63+
64+
"""
65+
1. User goes on their device(client) and the client sends a request to /device_authorization
66+
against the provider:
67+
https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
68+
https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
69+
70+
71+
POST /device_authorization HTTP/1.1
72+
Host: server.example.com
73+
Content-Type: application/x-www-form-urlencoded
74+
75+
client_id=1406020730&scope=example_scope
76+
77+
Response:
78+
HTTP/1.1 200 OK
79+
Content-Type: application/json
80+
Cache-Control: no-store
81+
82+
{
83+
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
84+
"user_code": "WDJB-MJHT",
85+
"verification_uri": "https://example.com/device",
86+
"verification_uri_complete":
87+
"https://example.com/device?user_code=WDJB-MJHT",
88+
"expires_in": 1800,
89+
"interval": 5
90+
}
91+
"""
92+
93+
94+
class DeviceAuthorizationEndpoint:
95+
@staticmethod
96+
def create_device_authorization_response(request):
97+
server = DeviceApplicationServer(interval=5, verification_uri="https://example.com/device")
98+
return server.create_device_authorization_response(request)
99+
100+
def post(self, request):
101+
headers, data, status = self.create_device_authorization_response(request)
102+
device_response = ...
103+
104+
# Create an instance of examples.device_flow.Device` using `request` and `data`that encapsulates
105+
# https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 &
106+
# https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
107+
108+
return device_response
109+
110+
111+
"""
112+
2. Client presents the information to the user
113+
(There's a section on non visual capable devices as well
114+
https://datatracker.ietf.org/doc/html/rfc8628#section-5.7)
115+
+-------------------------------------------------+
116+
| |
117+
| Scan the QR code or, using +------------+ |
118+
| a browser on another device, |[_].. . [_]| |
119+
| visit: | . .. . .| |
120+
| https://example.com/device | . . . ....| |
121+
| |. . . . | |
122+
| And enter the code: |[_]. ... . | |
123+
| WDJB-MJHT +------------+ |
124+
| |
125+
+-------------------------------------------------+
126+
"""
127+
# The implementation for step 2 is up to the owner of device.
128+
129+
130+
""""
131+
3 (The browser flow). User goes to https://example.com/device where they're presented with a
132+
form to fill in the user code.
133+
134+
Implement that endpoint on your provider and follow the logic in the rfc.
135+
136+
Making use of the errors in `oauthlib.oauth2.rfc8628.errors`
137+
138+
raise AccessDenied/AuthorizationPendingError/ExpiredTokenError where appropriate making use of
139+
`examples.device_flow.Device` to get and update current state of the device during the session
140+
141+
If the user isn't logged in(after inputting the user-code), they should be redirected to the provider's /login
142+
endpoint and redirected back to an /approve-deny endpoint(The name and implementation of /approve-deny is up
143+
to the owner of the provider, this is just an example).
144+
They should then see an "approve" or "deny" button to authorize the device.
145+
146+
Again, using `examples.device_flow.Device` to update the status appropriately during the session.
147+
"""
148+
# /device and /approve-deny is up to the owner of the provider to implement. Again, make sure to
149+
# keep referring to the rfc when implementing.
150+
151+
152+
"""
153+
4 (The polling flow)
154+
https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
155+
https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
156+
157+
158+
Right after step 2, the device polls the /token endpoint every "interval" amount of seconds
159+
to check if user has approved or denied the request.
160+
161+
When grant type is `urn:ietf:params:oauth:grant-type:device_code`,
162+
`oauthlib.oauth2.rfc8628.grant_types.device_code.DeviceCodeGrant` will be the handler
163+
that handles token generation.
164+
"""
165+
166+
167+
# This is purely for illustrative purposes
168+
# to demonstrate rate limiting on the token endpoint for the device flow.
169+
# It is up to as the provider to decide how you want
170+
# to rate limit the device during polling.
171+
def rate_limit(func, rate="1/5s"):
172+
def wrapper():
173+
# some logic to ensure client device is rate limited by a minimum
174+
# of 1 request every 5 seconds during device polling
175+
# https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
176+
177+
if rate > "1 request a second(1/5s)":
178+
raise device_flow_errors.SlowDownError()
179+
180+
result = func()
181+
return result
182+
183+
return wrapper
184+
185+
186+
class ExampleRequestValidator(RequestValidator):
187+
# All the other methods that need to be implemented...
188+
# see examples.skeleton_oauth2_web_application_server.SkeletonValidator
189+
# for a more complete example.
190+
191+
# Here our main concern is this method:
192+
def create_token_response(self): ...
193+
194+
195+
class ServerSetupForTokenEndpoint:
196+
def __init__(self):
197+
validator = ExampleRequestValidator
198+
self.server = Server(validator)
199+
200+
201+
# You should already have the /token endpoint implemented in your provider.
202+
class TokenEndpoint(ServerSetupForTokenEndpoint):
203+
def default_flow_token_response(self, request):
204+
url, headers, body, status = self.server.create_token_response(request)
205+
access_token = json.loads(body).get("access_token")
206+
207+
# return access_token in a http response
208+
return access_token
209+
210+
@rate_limit # this will raise the SlowDownError
211+
def device_flow_token_response(self, request, device_code):
212+
"""
213+
Following the rfc, this will route the device request accordingly and raise
214+
required errors.
215+
216+
Remember that unlike other auth flows, the device if polling this endpoint once
217+
every "interval" amount of seconds.
218+
"""
219+
# using device_code arg to retrieve the correct device object instance
220+
device = Device
221+
222+
if device.status == device.DeviceFlowStatus.AUTHORIZATION_PENDING:
223+
raise AuthorizationPendingError()
224+
225+
if device.status == device.status == device.DeviceFlowStatus.DENIED:
226+
raise AccessDenied()
227+
228+
url, headers, body, status = self.server.create_token_response(request)
229+
230+
access_token = json.loads(body).get("access_token")
231+
232+
device.status = device.EXPIRED
233+
234+
# return access_token in a http response
235+
return access_token
236+
237+
# Example of how token endpoint could handle the token creation depending on
238+
# the grant type during a POST to /token.
239+
def post(self, request):
240+
params = request.POST
241+
if params.get("grant_type") == "urn:ietf:params:oauth:grant-type:device_code":
242+
return self.device_flow_token_response(request, params["device_code"])
243+
return self.default_flow_token_response(request)

0 commit comments

Comments
 (0)