1
1
import json
2
2
3
3
from django import forms , http
4
- from django .contrib .auth .decorators import login_required
5
- from django .db . models import Q
6
- from django .shortcuts import render
4
+ from django .contrib .auth .mixins import LoginRequiredMixin
5
+ from django .core . exceptions import ValidationError
6
+ from django .shortcuts import get_object_or_404
7
7
from django .urls import reverse
8
8
from django .utils .decorators import method_decorator
9
9
from django .views .decorators .csrf import csrf_exempt
10
- from django .views .generic import View
10
+ from django .views .generic import DetailView , FormView , View
11
11
from oauthlib .oauth2 import DeviceApplicationServer
12
12
13
13
from oauth2_provider .compat import login_not_required
@@ -40,12 +40,43 @@ def post(self, request, *args, **kwargs):
40
40
return http .JsonResponse (data = response , status = status , headers = headers )
41
41
42
42
43
- class DeviceForm (forms .Form ):
43
+ class DeviceGrantForm (forms .Form ):
44
44
user_code = forms .CharField (required = True )
45
45
46
+ def clean_user_code (self ):
47
+ """
48
+ Performs validation on the user_code provided by the user and adds to the cleaned_data dict
49
+ the "device_grant" object associated with the user_code, which is useful to process the
50
+ response in the DeviceUserCodeView.
46
51
47
- @login_required
48
- def device_user_code_view (request ):
52
+ It can raise one of the following ValidationErrors, with the associated codes:
53
+
54
+ * incorrect_user_code: if a device grant associated with the user_code does not exist
55
+ * expired_user_code: if the device grant associated with the user_code has expired
56
+ * user_code_already_used: if the device grant associated with the user_code has been already
57
+ approved or denied. The only accepted state of the device grant is AUTHORIZATION_PENDING.
58
+ """
59
+ cleaned_data = super ().clean ()
60
+ user_code : str = cleaned_data ["user_code" ]
61
+ try :
62
+ device_grant : DeviceGrant = get_device_grant_model ().objects .get (user_code = user_code )
63
+ except DeviceGrant .DoesNotExist :
64
+ raise ValidationError ("Incorrect user code" , code = "incorrect_user_code" )
65
+
66
+ if device_grant .is_expired ():
67
+ raise ValidationError ("Expired user code" , code = "expired_user_code" )
68
+
69
+ # User of device has already made their decision for this device.
70
+ if device_grant .status != device_grant .AUTHORIZATION_PENDING :
71
+ raise ValidationError ("User code has already been used" , code = "user_code_already_used" )
72
+
73
+ # Make the device_grant available to the View, saving one additional db call.
74
+ cleaned_data ["device_grant" ] = device_grant
75
+
76
+ return user_code
77
+
78
+
79
+ class DeviceUserCodeView (LoginRequiredMixin , FormView ):
49
80
"""
50
81
The view where the user is instructed (by the device) to come to in order to
51
82
enter the user code. More details in this section of the RFC:
@@ -56,69 +87,111 @@ def device_user_code_view(request):
56
87
in regardless, to approve the device login we're making the decision here, for
57
88
simplicity, to require being logged in up front.
58
89
"""
59
- form = DeviceForm (request .POST )
60
90
61
- if request . method ! = "POST" :
62
- return render ( request , "oauth2_provider/device/user_code.html" , { "form" : form })
91
+ template_name = "oauth2_provider/device/user_code.html"
92
+ form_class = DeviceGrantForm
63
93
64
- if not form .is_valid ():
65
- form .add_error (None , "Form invalid" )
66
- return render (request , "oauth2_provider/device/user_code.html" , {"form" : form }, status = 400 )
94
+ def get_success_url (self ):
95
+ return reverse (
96
+ "oauth2_provider:device-confirm" ,
97
+ kwargs = {
98
+ "client_id" : self .device_grant .client_id ,
99
+ "user_code" : self .device_grant .user_code ,
100
+ },
101
+ )
67
102
68
- user_code : str = form .cleaned_data ["user_code" ]
69
- try :
70
- device : DeviceGrant = get_device_grant_model ().objects .get (user_code = user_code )
71
- except DeviceGrant .DoesNotExist :
72
- form .add_error ("user_code" , "Incorrect user code" )
73
- return render (request , "oauth2_provider/device/user_code.html" , {"form" : form }, status = 404 )
103
+ def form_valid (self , form ):
104
+ """
105
+ Sets the device_grant on the instance so that it can be accessed
106
+ in get_success_url. It comes in handy when users want to overwrite
107
+ get_success_url, redirecting to the URL with the URL params pointing
108
+ to the current device.
109
+ """
110
+ device_grant : DeviceGrant = form .cleaned_data ["device_grant" ]
74
111
75
- device .user = request .user
76
- device .save (update_fields = ["user" ])
112
+ device_grant .user = self . request .user
113
+ device_grant .save (update_fields = ["user" ])
77
114
78
- if device .is_expired ():
79
- form .add_error ("user_code" , "Expired user code" )
80
- return render (request , "oauth2_provider/device/user_code.html" , {"form" : form }, status = 400 )
115
+ self .device_grant = device_grant
81
116
82
- # User of device has already made their decision for this device
83
- if device .status != device .AUTHORIZATION_PENDING :
84
- form .add_error ("user_code" , "User code has already been used" )
85
- return render (request , "oauth2_provider/device/user_code.html" , {"form" : form }, status = 400 )
117
+ return super ().form_valid (form )
86
118
87
- # 308 to indicate we want to keep the redirect being a POST request
88
- return http .HttpResponsePermanentRedirect (
89
- reverse (
90
- "oauth2_provider:device-confirm" ,
91
- kwargs = {"client_id" : device .client_id , "user_code" : user_code },
92
- ),
93
- status = 308 ,
94
- )
95
-
96
-
97
- @login_required
98
- def device_confirm_view (request : http .HttpRequest , client_id : str , user_code : str ):
99
- try :
100
- device : DeviceGrant = get_device_grant_model ().objects .get (
101
- # there is a db index on client_id
102
- Q (client_id = client_id ) & Q (user_code = user_code )
119
+
120
+ class DeviceConfirmForm (forms .Form ):
121
+ """
122
+ Simple form for the user to approve or deny the device.
123
+ """
124
+
125
+ action = forms .CharField (required = True )
126
+
127
+
128
+ class DeviceConfirmView (LoginRequiredMixin , FormView ):
129
+ """
130
+ The view where the user approves or denies a device.
131
+ """
132
+
133
+ template_name = "oauth2_provider/device/accept_deny.html"
134
+ form_class = DeviceConfirmForm
135
+
136
+ def get_object (self ):
137
+ """
138
+ Returns the DeviceGrant object in the AUTHORIZATION_PENDING state identified
139
+ by the slugs client_id and user_code. Raises Http404 if not found.
140
+ """
141
+ client_id , user_code = self .kwargs .get ("client_id" ), self .kwargs .get ("user_code" )
142
+ return get_object_or_404 (
143
+ DeviceGrant ,
144
+ client_id = client_id ,
145
+ user_code = user_code ,
146
+ status = DeviceGrant .AUTHORIZATION_PENDING ,
103
147
)
104
- except DeviceGrant .DoesNotExist :
105
- return http .HttpResponseNotFound ("<h1>Device not found</h1>" )
106
-
107
- if device .status != device .AUTHORIZATION_PENDING :
108
- # AUTHORIZATION_PENDING is the only accepted state, anything else implies
109
- # that the user already approved/denied OR the deadline has passed (aka
110
- # expired)
111
- return http .HttpResponseBadRequest ("Invalid" )
112
-
113
- action = request .POST .get ("action" )
114
-
115
- if action == "accept" :
116
- device .status = device .AUTHORIZED
117
- device .save (update_fields = ["status" ])
118
- return http .HttpResponse ("approved" )
119
- elif action == "deny" :
120
- device .status = device .DENIED
121
- device .save (update_fields = ["status" ])
122
- return http .HttpResponse ("deny" )
123
-
124
- return render (request , "oauth2_provider/device/accept_deny.html" )
148
+
149
+ def get_success_url (self ):
150
+ return reverse (
151
+ "oauth2_provider:device-grant-status" ,
152
+ kwargs = {
153
+ "client_id" : self .kwargs ["client_id" ],
154
+ "user_code" : self .kwargs ["user_code" ],
155
+ },
156
+ )
157
+
158
+ def get (self , request , * args , ** kwargs ):
159
+ """
160
+ Enable GET requests for improved user experience. But validate that the URL params
161
+ are correct (i.e. there exists a device grant in the db that corresponds to the URL
162
+ params) by calling .get_object()
163
+ """
164
+ _ = self .get_object () # raises 404 if URL parameters are incorrect
165
+ return super ().get (request , args , kwargs )
166
+
167
+ def form_valid (self , form ):
168
+ """
169
+ Uses get_object() to retrieves the DeviceGrant object and updates its state
170
+ to authorized or denied, based on the user input.
171
+ """
172
+ device = self .get_object ()
173
+ action = form .cleaned_data ["action" ]
174
+
175
+ if action == "accept" :
176
+ device .status = device .AUTHORIZED
177
+ device .save (update_fields = ["status" ])
178
+ return super ().form_valid (form )
179
+ elif action == "deny" :
180
+ device .status = device .DENIED
181
+ device .save (update_fields = ["status" ])
182
+ return super ().form_valid (form )
183
+ else :
184
+ return http .HttpResponseBadRequest ()
185
+
186
+
187
+ class DeviceGrantStatusView (LoginRequiredMixin , DetailView ):
188
+ """
189
+ The view to display the status of a DeviceGrant.
190
+ """
191
+
192
+ model = DeviceGrant
193
+ template_name = "oauth2_provider/device/device_grant_status.html"
194
+
195
+ def get_object (self ):
196
+ client_id , user_code = self .kwargs .get ("client_id" ), self .kwargs .get ("user_code" )
197
+ return get_object_or_404 (DeviceGrant , client_id = client_id , user_code = user_code )
0 commit comments