33import logging
44from urllib .parse import parse_qsl , urlencode , urlparse
55
6+ from django import http
67from django .contrib .auth .mixins import LoginRequiredMixin
78from django .contrib .auth .views import redirect_to_login
89from django .http import HttpResponse
1213from django .views .decorators .csrf import csrf_exempt
1314from django .views .decorators .debug import sensitive_post_parameters
1415from django .views .generic import FormView , View
16+ from django_ratelimit .decorators import ratelimit
17+ from oauthlib .oauth2 .rfc8628 .errors import (
18+ AccessDenied ,
19+ AuthorizationPendingError ,
20+ )
21+
22+ from oauth2_provider .models import Device
1523
1624from ..compat import login_not_required
1725from ..exceptions import OAuthToolkitError
@@ -290,10 +298,13 @@ class TokenView(OAuthLibMixin, View):
290298 * Authorization code
291299 * Password
292300 * Client credentials
301+ * Device code flow (specifically for the device polling stage)
293302 """
294303
295304 @method_decorator (sensitive_post_parameters ("password" , "client_secret" ))
296- def post (self , request , * args , ** kwargs ):
305+ def authorization_flow_token_response (
306+ self , request : http .HttpRequest , * args , ** kwargs
307+ ) -> http .HttpResponse :
297308 url , headers , body , status = self .create_token_response (request )
298309 if status == 200 :
299310 access_token = json .loads (body ).get ("access_token" )
@@ -307,6 +318,38 @@ def post(self, request, *args, **kwargs):
307318 response [k ] = v
308319 return response
309320
321+ @method_decorator (ratelimit (key = "ip" , rate = f"1/{ oauth2_settings .DEVICE_FLOW_INTERVAL } " ))
322+ def device_flow_token_response (
323+ self , request : http .HttpRequest , device_code : str , * args , ** kwargs
324+ ) -> http .HttpResponse :
325+ device = Device .objects .get (device_code = device_code )
326+
327+ if device .status == device .AUTHORIZATION_PENDING :
328+ raise AuthorizationPendingError
329+
330+ if device .status == device .DENIED :
331+ raise AccessDenied
332+
333+ url , headers , body , status = self .create_token_response (request )
334+
335+ if status != 200 :
336+ return http .JsonResponse (data = json .loads (body ), status = status )
337+
338+ response = http .JsonResponse (data = json .loads (body ), status = status )
339+
340+ for k , v in headers .items ():
341+ response [k ] = v
342+
343+ device .status = device .EXPIRED
344+ device .save (update_fields = ["status" ])
345+ return response
346+
347+ def post (self , request : http .HttpRequest , * args , ** kwargs ) -> http .HttpResponse :
348+ params = request .POST
349+ if params .get ("grant_type" ) == "urn:ietf:params:oauth:grant-type:device_code" :
350+ return self .device_flow_token_response (request , params ["device_code" ])
351+ return self .authorization_flow_token_response (request )
352+
310353
311354@method_decorator (csrf_exempt , name = "dispatch" )
312355@method_decorator (login_not_required , name = "dispatch" )
0 commit comments