88from django .urls import reverse
99
1010from oauth2_provider .exceptions import BackchannelLogoutRequestError
11- from oauth2_provider .models import get_application_model , get_id_token_model
11+ from oauth2_provider .models import (
12+ get_application_model ,
13+ get_id_token_model ,
14+ get_access_token_model ,
15+ get_refresh_token_model ,
16+ )
1217from oauth2_provider .handlers import (
1318 on_user_logged_out_maybe_send_backchannel_logout ,
1419 send_backchannel_logout_request ,
2126
2227Application = get_application_model ()
2328IDToken = get_id_token_model ()
29+ AccessToken = get_access_token_model ()
30+ RefreshToken = get_refresh_token_model ()
2431User = get_user_model ()
2532
2633
@@ -45,6 +52,20 @@ def setUp(self):
4552 application = self .application , user = self .user , expires = expiration_date
4653 )
4754
55+ self .access_token = AccessToken .objects .create (
56+ user = self .user ,
57+ application = self .application ,
58+ token = "test_access_token" ,
59+ expires = now + datetime .timedelta (hours = 1 ),
60+ scope = "read write" , # No offline_access scope
61+ )
62+ self .refresh_token = RefreshToken .objects .create (
63+ user = self .user ,
64+ application = self .application ,
65+ token = "test_refresh_token" ,
66+ access_token = self .access_token ,
67+ )
68+
4869 def test_on_logout_handler_is_called_for_user (self ):
4970 with patch ("oauth2_provider.handlers.send_backchannel_logout_request" ) as backchannel_handler :
5071 self .client .login (username = "app_user" , password = "654321" )
@@ -78,3 +99,84 @@ def test_logout_sender_does_not_crash_on_backchannel_error(self):
7899 with patch ("oauth2_provider.handlers.send_backchannel_logout_request" ) as mock_func :
79100 mock_func .side_effect = BackchannelLogoutRequestError ("Bad Gateway" )
80101 on_user_logged_out_maybe_send_backchannel_logout (sender = User , user = self .user )
102+
103+ def test_no_logout_sent_when_only_offline_access_refresh_tokens (self ):
104+ # Add offline_access scope
105+ self .access_token .scope = "read write offline_access"
106+ self .access_token .save ()
107+
108+ with patch ("oauth2_provider.handlers.send_backchannel_logout_request" ) as backchannel_handler :
109+ on_user_logged_out_maybe_send_backchannel_logout (sender = User , user = self .user )
110+ backchannel_handler .assert_not_called ()
111+
112+ def test_logout_sent_when_refresh_token_without_offline_access (self ):
113+ with patch ("oauth2_provider.handlers.send_backchannel_logout_request" ) as backchannel_handler :
114+ on_user_logged_out_maybe_send_backchannel_logout (sender = User , user = self .user )
115+ backchannel_handler .assert_called_once ()
116+ _ , kwargs = backchannel_handler .call_args
117+ self .assertEqual (kwargs ["id_token" ], self .id_token )
118+
119+ def test_only_one_logout_per_application_with_multiple_refresh_tokens (self ):
120+ # Create another refresh token for the same application
121+ now = timezone .now ()
122+ another_access_token = AccessToken .objects .create (
123+ user = self .user ,
124+ application = self .application ,
125+ token = "test_access_token_2" ,
126+ expires = now + datetime .timedelta (hours = 1 ),
127+ scope = "read write" ,
128+ )
129+ RefreshToken .objects .create (
130+ user = self .user ,
131+ application = self .application ,
132+ token = "test_refresh_token_2" ,
133+ access_token = another_access_token ,
134+ )
135+
136+ # Should still be called only once despite having 2 refresh tokens
137+ with patch ("oauth2_provider.handlers.send_backchannel_logout_request" ) as backchannel_handler :
138+ on_user_logged_out_maybe_send_backchannel_logout (sender = User , user = self .user )
139+ backchannel_handler .assert_called_once ()
140+
141+ def test_logout_sent_for_multiple_applications (self ):
142+ # Create another application with backchannel logout URI
143+ another_app = Application .objects .create (
144+ name = "test_app_2" ,
145+ user = self .developer ,
146+ client_type = Application .CLIENT_PUBLIC ,
147+ authorization_grant_type = Application .GRANT_CLIENT_CREDENTIALS ,
148+ algorithm = Application .RS256_ALGORITHM ,
149+ client_secret = "another_secret" ,
150+ backchannel_logout_uri = "http://rp2.example.com/logout" ,
151+ )
152+
153+ # Create ID token for the second application
154+ another_id_token = IDToken .objects .create (
155+ application = another_app , user = self .user , expires = timezone .now () + datetime .timedelta (minutes = 180 )
156+ )
157+
158+ # Create access token and refresh token for the second application
159+ now = timezone .now ()
160+ another_access_token = AccessToken .objects .create (
161+ user = self .user ,
162+ application = another_app ,
163+ token = "test_access_token_app2" ,
164+ expires = now + datetime .timedelta (hours = 1 ),
165+ scope = "read write" ,
166+ )
167+ RefreshToken .objects .create (
168+ user = self .user ,
169+ application = another_app ,
170+ token = "test_refresh_token_app2" ,
171+ access_token = another_access_token ,
172+ )
173+
174+ # Should be called twice - once for each application - and both ID tokens were used
175+ with patch ("oauth2_provider.handlers.send_backchannel_logout_request" ) as backchannel_handler :
176+ on_user_logged_out_maybe_send_backchannel_logout (sender = User , user = self .user )
177+ self .assertEqual (backchannel_handler .call_count , 2 )
178+
179+ call_args_list = backchannel_handler .call_args_list
180+ id_tokens_called = [call .kwargs ["id_token" ] for call in call_args_list ]
181+ self .assertIn (self .id_token , id_tokens_called )
182+ self .assertIn (another_id_token , id_tokens_called )
0 commit comments