@@ -34,12 +34,17 @@ def setUpTestData(cls):
3434 name = "test_client_credentials_app" ,
3535 user = cls .dev_user ,
3636 client_type = Application .CLIENT_PUBLIC ,
37- authorization_grant_type = Application .GRANT_CLIENT_CREDENTIALS ,
37+ authorization_grant_type = Application .GRANT_DEVICE_CODE ,
3838 client_secret = "abcdefghijklmnopqrstuvwxyz1234567890" ,
3939 )
4040
4141
4242class TestDeviceFlow (BaseTest ):
43+ """
44+ The first 2 tests test the device flow in order
45+ how the device flow works
46+ """
47+
4348 @mock .patch (
4449 "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token" ,
4550 lambda : "abc" ,
@@ -96,6 +101,115 @@ def test_device_flow_authorization_initiation(self):
96101 "interval" : 5 ,
97102 }
98103
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+
99213 @mock .patch (
100214 "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token" ,
101215 lambda : "abc" ,
0 commit comments