@@ -217,13 +217,16 @@ def _load_application(self, client_id, request):
217217 if request .client :
218218 # check for cached client, to save the db hit if this has already been loaded
219219 if not isinstance (request .client , Application ):
220- # resetting request.client (client_id=%r): not an Application, something else set request.client erroneously
220+ # resetting request.client (client_id=%r): not an Application, something else set
221+ # request.client erroneously
221222 request .client = None
222223 elif request .client .client_id != client_id :
223- # resetting request.client (client_id=%r): request.client.client_id does not match the given client_id
224+ # resetting request.client (client_id=%r): request.client.client_id does not match
225+ # the given client_id
224226 request .client = None
225227 elif not request .client .is_usable (request ):
226- # resetting request.client (client_id=%r): request.client is a valid Application, but is not usable
228+ # resetting request.client (client_id=%r): request.client is a valid Application,
229+ # but is not usable
227230 request .client = None
228231 else :
229232 # request.client is a valid Application, reusing it
@@ -614,6 +617,81 @@ def _save_bearer_token(self, token, request, *args, **kwargs):
614617 if "scope" not in token :
615618 raise FatalClientError ("Failed to renew access token: missing scope" )
616619
620+ # RFC 8707: Extract resource parameter from request
621+ # For authorization_code grant, resource comes from the grant (already JSON-encoded)
622+ # but can be narrowed by the token request
623+ # For other grants, it comes from the request directly and needs encoding
624+ if request .grant_type == "authorization_code" :
625+ # Get resource from the grant that was validated
626+ grant = Grant .objects .filter (code = request .code , application = request .client ).first ()
627+ grant_resource = grant .resource if (grant and grant .resource ) else ""
628+
629+ # Check if token request specifies a subset of resources
630+ requested_resource = getattr (request , "resource" , None )
631+ if requested_resource :
632+ # RFC 8707: Token request is narrowing the resource scope
633+ # Validate that requested resources are a subset of granted resources
634+ if isinstance (requested_resource , str ):
635+ requested_list = [requested_resource ]
636+ else :
637+ requested_list = requested_resource
638+
639+ # Parse granted resources
640+ if grant_resource :
641+ try :
642+ granted_list = json .loads (grant_resource )
643+ except (json .JSONDecodeError , TypeError ):
644+ granted_list = []
645+ else :
646+ granted_list = []
647+
648+ # Validate that all requested resources were granted
649+ if granted_list : # Only validate if resources were originally granted
650+ for res in requested_list :
651+ if res not in granted_list :
652+ # RFC 8707: Use invalid_target error per spec
653+ raise errors .CustomOAuth2Error (
654+ error = "invalid_target" ,
655+ description = (
656+ f"Requested resource '{ res } ' was not included in the "
657+ "original authorization grant"
658+ ),
659+ request = request ,
660+ )
661+
662+ request .resource = json .dumps (requested_list )
663+ elif grant_resource :
664+ # Use all resources from the grant
665+ request .resource = grant_resource
666+ else :
667+ request .resource = ""
668+ else :
669+ # For other grant types (client_credentials, password, implicit, etc.)
670+ # Extract resource from request and JSON-encode it if needed
671+ resource = getattr (request , "resource" , None )
672+ if resource :
673+ # Check if already JSON-encoded (from authorization endpoint)
674+ # vs raw from token endpoint
675+ if isinstance (resource , str ):
676+ # Could be either a single URI or already JSON-encoded
677+ try :
678+ # Try to parse as JSON
679+ parsed = json .loads (resource )
680+ if isinstance (parsed , list ):
681+ # Already JSON-encoded, use as-is
682+ request .resource = resource
683+ else :
684+ # Single URI, needs encoding
685+ request .resource = json .dumps ([resource ])
686+ except (json .JSONDecodeError , TypeError ):
687+ # Not JSON, it's a single URI
688+ request .resource = json .dumps ([resource ])
689+ else :
690+ # It's a list, encode it
691+ request .resource = json .dumps (resource )
692+ else :
693+ request .resource = ""
694+
617695 # expires_in is passed to Server on initialization
618696 # custom server class can have logic to override this
619697 expires = timezone .now () + timedelta (
@@ -717,11 +795,22 @@ def _create_access_token(self, expires, request, token, source_refresh_token=Non
717795 id_token = id_token ,
718796 application = request .client ,
719797 source_refresh_token = source_refresh_token ,
798+ resource = getattr (request , "resource" , "" ), # RFC 8707
720799 )
721800
722801 def _create_authorization_code (self , request , code , expires = None ):
723802 if not expires :
724803 expires = timezone .now () + timedelta (seconds = oauth2_settings .AUTHORIZATION_CODE_EXPIRE_SECONDS )
804+
805+ # RFC 8707: Extract resource parameter
806+ # The resource parameter is already JSON-encoded from the view
807+ resource = getattr (request , "resource" , None )
808+ if resource :
809+ # Resource is already JSON-encoded from the view, just use it
810+ resource_json = resource
811+ else :
812+ resource_json = ""
813+
725814 return Grant .objects .create (
726815 application = request .client ,
727816 user = request .user ,
@@ -733,6 +822,7 @@ def _create_authorization_code(self, request, code, expires=None):
733822 code_challenge_method = request .code_challenge_method or "" ,
734823 nonce = request .nonce or "" ,
735824 claims = json .dumps (request .claims or {}),
825+ resource = resource_json ,
736826 )
737827
738828 def _create_refresh_token (self , request , refresh_token_code , access_token , previous_refresh_token ):
0 commit comments