@@ -985,6 +985,53 @@ def test_refresh_fail_repeating_requests(self):
985
985
response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
986
986
self .assertEqual (response .status_code , 400 )
987
987
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
+ response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1017
+ self .assertEqual (response .status_code , 200 )
1018
+ new_tokens = json .loads (response .content .decode ("utf-8" ))
1019
+
1020
+ # Second request fails
1021
+ response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1022
+ self .assertEqual (response .status_code , 400 )
1023
+
1024
+ # Previously returned tokens are now invalid
1025
+ new_token_request_data = {
1026
+ "grant_type" : "refresh_token" ,
1027
+ "refresh_token" : new_tokens ["refresh_token" ],
1028
+ "scope" : new_tokens ["scope" ],
1029
+ }
1030
+ response = self .client .post (
1031
+ reverse ("oauth2_provider:token" ), data = new_token_request_data , ** auth_headers
1032
+ )
1033
+ self .assertEqual (response .status_code , 400 )
1034
+
988
1035
def test_refresh_repeating_requests (self ):
989
1036
"""
990
1037
Trying to refresh an access token with the same refresh token more than
@@ -1024,6 +1071,63 @@ def test_refresh_repeating_requests(self):
1024
1071
response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1025
1072
self .assertEqual (response .status_code , 400 )
1026
1073
1074
+ def test_refresh_repeating_requests_grace_period_with_reuse_protection (self ):
1075
+ """
1076
+ Trying to refresh an access token with the same refresh token more than
1077
+ once succeeds. Should work within the grace period, but should revoke previous tokens
1078
+ """
1079
+ self .oauth2_settings .REFRESH_TOKEN_GRACE_PERIOD_SECONDS = 120
1080
+ self .oauth2_settings .REFRESH_TOKEN_REUSE_PROTECTION = True
1081
+ self .client .login (username = "test_user" , password = "123456" )
1082
+ authorization_code = self .get_auth ()
1083
+
1084
+ token_request_data = {
1085
+ "grant_type" : "authorization_code" ,
1086
+ "code" : authorization_code ,
1087
+ "redirect_uri" : "http://example.org" ,
1088
+ }
1089
+ auth_headers = get_basic_auth_header (self .application .client_id , CLEARTEXT_SECRET )
1090
+
1091
+ response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1092
+ content = json .loads (response .content .decode ("utf-8" ))
1093
+ self .assertTrue ("refresh_token" in content )
1094
+
1095
+ refresh_token_1 = content ["refresh_token" ]
1096
+ token_request_data = {
1097
+ "grant_type" : "refresh_token" ,
1098
+ "refresh_token" : refresh_token_1 ,
1099
+ "scope" : content ["scope" ],
1100
+ }
1101
+ response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1102
+ self .assertEqual (response .status_code , 200 )
1103
+ refresh_token_2 = json .loads (response .content .decode ("utf-8" ))["refresh_token" ]
1104
+
1105
+ response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1106
+ self .assertEqual (response .status_code , 200 )
1107
+ refresh_token_3 = json .loads (response .content .decode ("utf-8" ))["refresh_token" ]
1108
+
1109
+ self .assertEqual (refresh_token_2 , refresh_token_3 )
1110
+
1111
+ # Let the first refresh token expire
1112
+ rt = RefreshToken .objects .get (token = refresh_token_1 )
1113
+ rt .revoked = timezone .now () - datetime .timedelta (minutes = 10 )
1114
+ rt .save ()
1115
+
1116
+ # Using the expired token fails
1117
+ response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1118
+ self .assertEqual (response .status_code , 400 )
1119
+
1120
+ # Because we used the expired token, the recently issued token is also revoked
1121
+ new_token_request_data = {
1122
+ "grant_type" : "refresh_token" ,
1123
+ "refresh_token" : refresh_token_2 ,
1124
+ "scope" : content ["scope" ],
1125
+ }
1126
+ response = self .client .post (
1127
+ reverse ("oauth2_provider:token" ), data = new_token_request_data , ** auth_headers
1128
+ )
1129
+ self .assertEqual (response .status_code , 400 )
1130
+
1027
1131
def test_refresh_repeating_requests_non_rotating_tokens (self ):
1028
1132
"""
1029
1133
Try refreshing an access token with the same refresh token more than once when not rotating tokens.
0 commit comments