@@ -985,6 +985,54 @@ 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
+ # 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
+
988
1036
def test_refresh_repeating_requests (self ):
989
1037
"""
990
1038
Trying to refresh an access token with the same refresh token more than
@@ -1024,6 +1072,63 @@ def test_refresh_repeating_requests(self):
1024
1072
response = self .client .post (reverse ("oauth2_provider:token" ), data = token_request_data , ** auth_headers )
1025
1073
self .assertEqual (response .status_code , 400 )
1026
1074
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
+
1027
1132
def test_refresh_repeating_requests_non_rotating_tokens (self ):
1028
1133
"""
1029
1134
Try refreshing an access token with the same refresh token more than once when not rotating tokens.
0 commit comments