@@ -34,12 +34,17 @@ def setUpTestData(cls):
34
34
name = "test_client_credentials_app" ,
35
35
user = cls .dev_user ,
36
36
client_type = Application .CLIENT_PUBLIC ,
37
- authorization_grant_type = Application .GRANT_CLIENT_CREDENTIALS ,
37
+ authorization_grant_type = Application .GRANT_DEVICE_CODE ,
38
38
client_secret = "abcdefghijklmnopqrstuvwxyz1234567890" ,
39
39
)
40
40
41
41
42
42
class TestDeviceFlow (BaseTest ):
43
+ """
44
+ The first 2 tests test the device flow in order
45
+ how the device flow works
46
+ """
47
+
43
48
@mock .patch (
44
49
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token" ,
45
50
lambda : "abc" ,
@@ -96,6 +101,115 @@ def test_device_flow_authorization_initiation(self):
96
101
"interval" : 5 ,
97
102
}
98
103
104
+ @mock .patch (
105
+ "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token" ,
106
+ lambda : "abc" ,
107
+ )
108
+ def test_device_flow_authorization_user_code_confirm_and_access_token (self ):
109
+ """
110
+ 1. User visits the /device endpoint in their browsers and submits the user code
111
+
112
+ the device and approve deny actions occur concurrently
113
+ (i.e the device is polling the token endpoint while the user
114
+ either approves or denies the device)
115
+
116
+ -2(3)-. User approves or denies the device
117
+ -3(2)-. Device polls the /token endpoint
118
+ """
119
+
120
+ # -----------------------
121
+ # 0: Setup device flow
122
+ # -----------------------
123
+ self .oauth2_settings .OAUTH_DEVICE_VERIFICATION_URI = "example.com/device"
124
+ self .oauth2_settings .OAUTH_DEVICE_USER_CODE_GENERATOR = lambda : "xyz"
125
+
126
+ request_data : dict [str , str ] = {
127
+ "client_id" : self .application .client_id ,
128
+ }
129
+ request_as_x_www_form_urlencoded : str = urlencode (request_data )
130
+
131
+ django .http .response .JsonResponse = self .client .post (
132
+ reverse ("oauth2_provider:device-authorization" ),
133
+ data = request_as_x_www_form_urlencoded ,
134
+ content_type = "application/x-www-form-urlencoded" ,
135
+ )
136
+
137
+ # /device and /device_confirm require a user to be logged in
138
+ # to access it
139
+ UserModel .objects .create_user (
140
+ username = "test_user_device_flow" ,
141
+
142
+ password = "password123" ,
143
+ )
144
+ self .client .login (username = "test_user_device_flow" , password = "password123" )
145
+
146
+ # --------------------------------------------------------------------------------
147
+ # 1. User visits the /device endpoint in their browsers and submits the user code
148
+ # submits wrong code then right code
149
+ # --------------------------------------------------------------------------------
150
+
151
+ # 1. User visits the /device endpoint in their browsers and submits the user code
152
+ # (GET Request to load it)
153
+ get_response = self .client .get (reverse ("oauth2_provider:device" ))
154
+ assert get_response .status_code == 200
155
+ assert "form" in get_response .context # Ensure the form is rendered in the context
156
+
157
+ # 1.1.0 User visits the /device endpoint in their browsers and submits wrong user code
158
+ with pytest .raises (oauth2_provider .models .Device .DoesNotExist ):
159
+ self .client .post (
160
+ reverse ("oauth2_provider:device" ),
161
+ data = {"user_code" : "invalid_code" },
162
+ )
163
+
164
+ # 1.1.1: user submits valid user code
165
+ post_response_valid = self .client .post (
166
+ reverse ("oauth2_provider:device" ),
167
+ data = {"user_code" : "xyz" },
168
+ )
169
+
170
+ device_confirm_url = reverse ("oauth2_provider:device-confirm" , kwargs = {"device_code" : "abc" })
171
+ assert post_response_valid .status_code == 308 # Ensure it redirects with 308 status
172
+ assert post_response_valid ["Location" ] == device_confirm_url
173
+
174
+ device_confirm_url = reverse ("oauth2_provider:device-confirm" , kwargs = {"device_code" : "abc" })
175
+ assert post_response_valid ["Location" ] == device_confirm_url
176
+
177
+ # --------------------------------------------------------------------------------
178
+ # 2: We redirect to the accept/deny form (the user is still in their browser)
179
+ # and approves
180
+ # --------------------------------------------------------------------------------
181
+ get_confirm = self .client .get (device_confirm_url )
182
+ assert get_confirm .status_code == 200
183
+
184
+ approve_response = self .client .post (device_confirm_url , data = {"action" : "accept" })
185
+ assert approve_response .status_code == 200
186
+ assert approve_response .content .decode () == "approved"
187
+
188
+ device = DeviceModel .objects .get (device_code = "abc" )
189
+ assert device .status == device .AUTHORIZED
190
+
191
+ # -------------------------
192
+ # 3: Device polls /token
193
+ # -------------------------
194
+ token_payload = {
195
+ "device_code" : device .device_code ,
196
+ "client_id" : self .application .client_id ,
197
+ "grant_type" : "urn:ietf:params:oauth:grant-type:device_code" ,
198
+ }
199
+ token_response = self .client .post (
200
+ reverse ("oauth2_provider:token" ),
201
+ data = urlencode (token_payload ),
202
+ content_type = "application/x-www-form-urlencoded" ,
203
+ )
204
+
205
+ assert token_response .status_code == 200
206
+
207
+ token_data = token_response .json ()
208
+
209
+ assert "access_token" in token_data
210
+ assert token_data ["token_type" ].lower () == "bearer"
211
+ assert "scope" in token_data
212
+
99
213
@mock .patch (
100
214
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token" ,
101
215
lambda : "abc" ,
0 commit comments