@@ -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