Skip to content

Commit c3096d3

Browse files
committed
Add tutotial doc
1 parent 67dafbf commit c3096d3

File tree

3 files changed

+145
-1
lines changed

3 files changed

+145
-1
lines changed
Loading

docs/tutorial/tutorial.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ Tutorials
99
tutorial_03
1010
tutorial_04
1111
tutorial_05
12-
12+
tutorial_06

docs/tutorial/tutorial_06.rst

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
Device authorization grant flow
2+
====================================================
3+
4+
Scenario
5+
--------
6+
In :doc:`Part 1 <tutorial_01>` you created your own :term:`Authorization Server` and it's running along just fine.
7+
You have devices that your users have and those users need to authenticate the device against your
8+
:term:`Authorization Server` in order to make the required api calls.
9+
10+
Device Authorization
11+
-----------------
12+
The OAuth 2.0 device authorization grant is designed for Internet
13+
connected devices that either lack a browser to perform a user-agent
14+
based authorization or are input constrained to the extent that
15+
requiring the user to input text in order to authenticate during the
16+
authorization flow is impractical. It enables OAuth clients on such
17+
devices (like smart TVs, media consoles, digital picture frames, and
18+
printers) to obtain user authorization to access protected resources
19+
by using a user agent on a separate device.
20+
21+
22+
Point your browser to http://127.0.0.1:8000/o/applications/register/ create an application.
23+
24+
Fill the form as show in the screenshot below, and before saving take note of ``Client id``.
25+
Make sure the client type is set to "Public". There are cases where a confidential client makes sense
26+
but generally, it assumed the device is unable to safely store the client secret.
27+
28+
.. image:: _images/application-register-device-code.png
29+
:alt: Device Authorization application registration
30+
31+
Ensure the setting OAUTH_DEVICE_VERIFICATION_URI is set to a uri you want to come back
32+
verification_uri key in the response. This is what the device will use display
33+
to the user
34+
35+
.. code-block:: sh
36+
curl -X POST \
37+
--header 'Content-Type: application/x-www-form-urlencoded' \
38+
"http://localhost:8008/o/device_authorization/" \
39+
--data-urlencode 'client_id=${CLIENT_ID}}' \
40+
--data-urlencode 'scope={your scope}'
41+
42+
The OAuth2 provider will return the following response:
43+
44+
.. code-block:: json
45+
{
46+
"verification_uri": "example.com/device",
47+
"expires_in": 1800,
48+
"user_code": "12345",
49+
"device_code": "12345",
50+
"interval": 5
51+
}
52+
53+
You will now need to implement your own /device and /device_confirm endpoints in
54+
your own :term:`Authorization Server`.((the name of these endpoints are up to you) It should follow the RFC but device flow implementation usually
55+
extend beyond the RFC on authorization server to authorization server basis.
56+
57+
You may see some implementations out there add an extra openid connect layer on top of this oauth 2 device flow
58+
for example.
59+
60+
/device endpoint
61+
-------------
62+
63+
following the rfc(https://datatracker.ietf.org/doc/html/rfc8628#section-3.3)
64+
your implementation may look like this
65+
66+
.. code-block:: python
67+
from oauthlib.oauth2.rfc8628.errors import (
68+
AccessDenied,
69+
AuthorizationPendingError,
70+
ExpiredTokenError,
71+
)
72+
from oauth2_provider.models import Device, get_device_model
73+
74+
class DeviceForm(forms.Form):
75+
user_code = forms.CharField(required=True)
76+
77+
def device_view(request, **kwargs):
78+
form = forms.DeviceForm(request.POST)
79+
80+
if request.method != "POST":
81+
# deny the request
82+
83+
user_code = form.cleaned_data['user_code']
84+
85+
# there should only be one
86+
device: Device = get_device_model().objects.get(user_code=user_code)
87+
88+
if datetime.now(tz=UTC) > device.expires:
89+
device.status = device.EXPIRED
90+
device.save(update_fields=['status']) # this is saving to the db
91+
raise ExpiredTokenError()
92+
93+
# If the decisions to approve/deny has already been made, the flow is considered done
94+
if device.status in (device.DENIED, device.AUTHORIZED):
95+
raise AccessDenied()
96+
97+
# Add a check to make sure the user is logged into their account
98+
# as the /device endpoint is public
99+
if request.user.is_authenticated is False:
100+
# redirect to your app's login page
101+
return http.HttpResponseRedirect(...)
102+
103+
# user is logged in and typed the user code in correctly. redirect to the the approve deny endpoint now
104+
return http.HttpResponseRedirect(reverse("device-confirm", kwargs={"device_code": device.device_code}))
105+
106+
107+
/Device polling [user approving or denying happens concurrently]
108+
-------------
109+
110+
Note: You should already have the /token endpoint implemented in your authorization server before this.
111+
112+
After your app redirects to the device confirm/approve deny endpoint, your device must poll the authorization server's
113+
/token endpoint every "interval" amount of seconds to check if the user has approved or denied it. If approved
114+
your authorization server should return the access token back to the device.
115+
116+
The /token endpoint should be behind a rate limiter that falls in line with the DEVICE_FLOW_INTERVAL setting.
117+
118+
119+
/device-confirm endpoint [device polling is happening concurrently]
120+
-------------
121+
The device confirm endpoint may look like this:
122+
123+
.. code-block:: python
124+
def device_confirm(request: http.HttpRequest, device_code: str):
125+
device: Device = get_device_model().objects.get(device_code=device_code)
126+
127+
if device.status in (device.AUTHORIZED, device.DENIED):
128+
return http.HttpResponse("Invalid")
129+
130+
if request.user.is_authenticated is False:
131+
return http.HttpResponse("Nope")
132+
133+
action = request.POST.get('action')
134+
135+
if action == "accept":
136+
device.status = device.AUTHORIZED
137+
device.save(update_fields=["status"])
138+
return http.HttpResponse("approved")
139+
elif action == "deny":
140+
device.status = device.DENIED
141+
device.save(update_fields=["status"])
142+
return http.HttpResponse("deny")
143+
144+
return render(request, 'authserver/accept_deny_device_access.html')

0 commit comments

Comments
 (0)