@@ -985,6 +985,54 @@ def test_refresh_fail_repeating_requests(self):
985985        response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
986986        self .assertEqual (response .status_code , 400 )
987987
988+     def  test_refresh_repeating_requests_revokes_old_token (self ):
989+         """ 
990+         If a refresh token is reused, the server should invalidate *all* access tokens that have a relation 
991+         to the re-used token. This forces a malicious actor to be logged out. 
992+         The server can't determine whether the first or the second client was legitimate, so it needs to 
993+         revoke both. 
994+         See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-29#name-recommendations 
995+         """ 
996+         self .oauth2_settings .REFRESH_TOKEN_REUSE_PROTECTION  =  True 
997+         self .client .login (username = "test_user" , password = "123456" )
998+         authorization_code  =  self .get_auth ()
999+ 
1000+         token_request_data  =  {
1001+             "grant_type" : "authorization_code" ,
1002+             "code" : authorization_code ,
1003+             "redirect_uri" : "http://example.org" ,
1004+         }
1005+         auth_headers  =  get_basic_auth_header (self .application .client_id , CLEARTEXT_SECRET )
1006+ 
1007+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1008+         content  =  json .loads (response .content .decode ("utf-8" ))
1009+         self .assertTrue ("refresh_token"  in  content )
1010+ 
1011+         token_request_data  =  {
1012+             "grant_type" : "refresh_token" ,
1013+             "refresh_token" : content ["refresh_token" ],
1014+             "scope" : content ["scope" ],
1015+         }
1016+         # First response works as usual 
1017+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1018+         self .assertEqual (response .status_code , 200 )
1019+         new_tokens  =  json .loads (response .content .decode ("utf-8" ))
1020+ 
1021+         # Second request fails 
1022+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1023+         self .assertEqual (response .status_code , 400 )
1024+ 
1025+         # Previously returned tokens are now invalid as well 
1026+         new_token_request_data  =  {
1027+             "grant_type" : "refresh_token" ,
1028+             "refresh_token" : new_tokens ["refresh_token" ],
1029+             "scope" : new_tokens ["scope" ],
1030+         }
1031+         response  =  self .client .post (
1032+             reverse ("oauth2_provider:token" ), data = new_token_request_data , ** auth_headers 
1033+         )
1034+         self .assertEqual (response .status_code , 400 )
1035+ 
9881036    def  test_refresh_repeating_requests (self ):
9891037        """ 
9901038        Trying to refresh an access token with the same refresh token more than 
@@ -1024,6 +1072,63 @@ def test_refresh_repeating_requests(self):
10241072        response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
10251073        self .assertEqual (response .status_code , 400 )
10261074
1075+     def  test_refresh_repeating_requests_grace_period_with_reuse_protection (self ):
1076+         """ 
1077+         Trying to refresh an access token with the same refresh token more than 
1078+         once succeeds. Should work within the grace period, but should revoke previous tokens 
1079+         """ 
1080+         self .oauth2_settings .REFRESH_TOKEN_GRACE_PERIOD_SECONDS  =  120 
1081+         self .oauth2_settings .REFRESH_TOKEN_REUSE_PROTECTION  =  True 
1082+         self .client .login (username = "test_user" , password = "123456" )
1083+         authorization_code  =  self .get_auth ()
1084+ 
1085+         token_request_data  =  {
1086+             "grant_type" : "authorization_code" ,
1087+             "code" : authorization_code ,
1088+             "redirect_uri" : "http://example.org" ,
1089+         }
1090+         auth_headers  =  get_basic_auth_header (self .application .client_id , CLEARTEXT_SECRET )
1091+ 
1092+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1093+         content  =  json .loads (response .content .decode ("utf-8" ))
1094+         self .assertTrue ("refresh_token"  in  content )
1095+ 
1096+         refresh_token_1  =  content ["refresh_token" ]
1097+         token_request_data  =  {
1098+             "grant_type" : "refresh_token" ,
1099+             "refresh_token" : refresh_token_1 ,
1100+             "scope" : content ["scope" ],
1101+         }
1102+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1103+         self .assertEqual (response .status_code , 200 )
1104+         refresh_token_2  =  json .loads (response .content .decode ("utf-8" ))["refresh_token" ]
1105+ 
1106+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1107+         self .assertEqual (response .status_code , 200 )
1108+         refresh_token_3  =  json .loads (response .content .decode ("utf-8" ))["refresh_token" ]
1109+ 
1110+         self .assertEqual (refresh_token_2 , refresh_token_3 )
1111+ 
1112+         # Let the first refresh token expire 
1113+         rt  =  RefreshToken .objects .get (token = refresh_token_1 )
1114+         rt .revoked  =  timezone .now () -  datetime .timedelta (minutes = 10 )
1115+         rt .save ()
1116+ 
1117+         # Using the expired token fails 
1118+         response  =  self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1119+         self .assertEqual (response .status_code , 400 )
1120+ 
1121+         # Because we used the expired token, the recently issued token is also revoked 
1122+         new_token_request_data  =  {
1123+             "grant_type" : "refresh_token" ,
1124+             "refresh_token" : refresh_token_2 ,
1125+             "scope" : content ["scope" ],
1126+         }
1127+         response  =  self .client .post (
1128+             reverse ("oauth2_provider:token" ), data = new_token_request_data , ** auth_headers 
1129+         )
1130+         self .assertEqual (response .status_code , 400 )
1131+ 
10271132    def  test_refresh_repeating_requests_non_rotating_tokens (self ):
10281133        """ 
10291134        Try refreshing an access token with the same refresh token more than once when not rotating tokens. 
0 commit comments