Skip to content

Commit 29da530

Browse files
tonialfrancoisfreitagpre-commit-ci[bot]
authored
Fix RP-initiated Logout with expired Django session (#1270)
* Fix RP-initiated Logout with exired django session * Update tests/test_oidc_views.py Co-authored-by: François Freitag <[email protected]> * Update tests/test_oidc_views.py Co-authored-by: François Freitag <[email protected]> * Update tests/test_oidc_views.py Co-authored-by: François Freitag <[email protected]> * Check post logout redirection * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: François Freitag <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 11294ab commit 29da530

File tree

4 files changed

+63
-15
lines changed

4 files changed

+63
-15
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Allisson Azevedo
1818
Andrea Greco
1919
Andrej Zbín
2020
Andrew Chen Wang
21+
Antoine Laurent
2122
Anvesh Agarwal
2223
Aristóbulo Meneses
2324
Aryan Iyappan

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
* #1211 documentation improve on 'AUTHORIZATION_CODE_EXPIRE_SECONDS'.
2525
* #1218 Confim support for Python 3.11.
2626
* #1222 Remove expired ID tokens alongside access tokens in `cleartokens` management command
27+
* #1270 Fix RP-initiated Logout with no available Django session
2728

2829
## [2.2.0] 2022-10-18
2930

oauth2_provider/views/oidc.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,27 +210,31 @@ def _validate_claims(request, claims):
210210
def validate_logout_request(request, id_token_hint, client_id, post_logout_redirect_uri):
211211
"""
212212
Validate an OIDC RP-Initiated Logout Request.
213-
`(prompt_logout, (post_logout_redirect_uri, application))` is returned.
213+
`(prompt_logout, (post_logout_redirect_uri, application), token_user)` is returned.
214214
215215
`prompt_logout` indicates whether the logout has to be confirmed by the user. This happens if the
216216
specifications force a confirmation, or it is enabled by `OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT`.
217217
`post_logout_redirect_uri` is the validated URI where the User should be redirected to after the
218218
logout. Can be None. None will redirect to "/" of this app. If it is set `application` will also
219-
be set to the Application that is requesting the logout.
219+
be set to the Application that is requesting the logout. `token_user` is the id_token user, which will
220+
used to revoke the tokens if found.
220221
221222
The `id_token_hint` will be validated if given. If both `client_id` and `id_token_hint` are given they
222223
will be validated against each other.
223224
"""
224225

225226
id_token = None
226227
must_prompt_logout = True
228+
token_user = None
227229
if id_token_hint:
228230
# Only basic validation has been done on the IDToken at this point.
229231
id_token, claims = _load_id_token(id_token_hint)
230232

231233
if not id_token or not _validate_claims(request, claims):
232234
raise InvalidIDTokenError()
233235

236+
token_user = id_token.user
237+
234238
if id_token.user == request.user:
235239
# A logout without user interaction (i.e. no prompt) is only allowed
236240
# if an ID Token is provided that matches the current user.
@@ -268,7 +272,7 @@ def validate_logout_request(request, id_token_hint, client_id, post_logout_redir
268272
if not application.post_logout_redirect_uri_allowed(post_logout_redirect_uri):
269273
raise InvalidOIDCRedirectURIError("This client does not have this redirect uri registered.")
270274

271-
return prompt_logout, (post_logout_redirect_uri, application)
275+
return prompt_logout, (post_logout_redirect_uri, application), token_user
272276

273277

274278
class RPInitiatedLogoutView(OIDCLogoutOnlyMixin, FormView):
@@ -309,7 +313,7 @@ def get(self, request, *args, **kwargs):
309313
state = request.GET.get("state")
310314

311315
try:
312-
prompt, (redirect_uri, application) = validate_logout_request(
316+
prompt, (redirect_uri, application), token_user = validate_logout_request(
313317
request=request,
314318
id_token_hint=id_token_hint,
315319
client_id=client_id,
@@ -319,7 +323,7 @@ def get(self, request, *args, **kwargs):
319323
return self.error_response(error)
320324

321325
if not prompt:
322-
return self.do_logout(application, redirect_uri, state)
326+
return self.do_logout(application, redirect_uri, state, token_user)
323327

324328
self.oidc_data = {
325329
"id_token_hint": id_token_hint,
@@ -341,28 +345,28 @@ def form_valid(self, form):
341345
state = form.cleaned_data.get("state")
342346

343347
try:
344-
prompt, (redirect_uri, application) = validate_logout_request(
348+
prompt, (redirect_uri, application), token_user = validate_logout_request(
345349
request=self.request,
346350
id_token_hint=id_token_hint,
347351
client_id=client_id,
348352
post_logout_redirect_uri=post_logout_redirect_uri,
349353
)
350354

351355
if not prompt or form.cleaned_data.get("allow"):
352-
return self.do_logout(application, redirect_uri, state)
356+
return self.do_logout(application, redirect_uri, state, token_user)
353357
else:
354358
raise LogoutDenied()
355359

356360
except OIDCError as error:
357361
return self.error_response(error)
358362

359-
def do_logout(self, application=None, post_logout_redirect_uri=None, state=None):
363+
def do_logout(self, application=None, post_logout_redirect_uri=None, state=None, token_user=None):
360364
# Delete Access Tokens
361365
if oauth2_settings.OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS:
362366
AccessToken = get_access_token_model()
363367
RefreshToken = get_refresh_token_model()
364368
access_tokens_to_delete = AccessToken.objects.filter(
365-
user=self.request.user,
369+
user=token_user or self.request.user,
366370
application__client_type__in=self.token_deletion_client_types,
367371
application__authorization_grant_type__in=self.token_deletion_grant_types,
368372
)

tests/test_oidc_views.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.test import RequestFactory, TestCase
55
from django.urls import reverse
66
from django.utils import timezone
7+
from pytest_django.asserts import assertRedirects
78

89
from oauth2_provider.exceptions import ClientIdMissmatch, InvalidOIDCClientError, InvalidOIDCRedirectURIError
910
from oauth2_provider.models import get_access_token_model, get_id_token_model, get_refresh_token_model
@@ -197,37 +198,37 @@ def test_validate_logout_request(oidc_tokens, public_application, other_user, rp
197198
id_token_hint=None,
198199
client_id=None,
199200
post_logout_redirect_uri=None,
200-
) == (True, (None, None))
201+
) == (True, (None, None), None)
201202
assert validate_logout_request(
202203
request=mock_request_for(oidc_tokens.user),
203204
id_token_hint=None,
204205
client_id=client_id,
205206
post_logout_redirect_uri=None,
206-
) == (True, (None, application))
207+
) == (True, (None, application), None)
207208
assert validate_logout_request(
208209
request=mock_request_for(oidc_tokens.user),
209210
id_token_hint=None,
210211
client_id=client_id,
211212
post_logout_redirect_uri="http://example.org",
212-
) == (True, ("http://example.org", application))
213+
) == (True, ("http://example.org", application), None)
213214
assert validate_logout_request(
214215
request=mock_request_for(oidc_tokens.user),
215216
id_token_hint=id_token,
216217
client_id=None,
217218
post_logout_redirect_uri="http://example.org",
218-
) == (ALWAYS_PROMPT, ("http://example.org", application))
219+
) == (ALWAYS_PROMPT, ("http://example.org", application), oidc_tokens.user)
219220
assert validate_logout_request(
220221
request=mock_request_for(other_user),
221222
id_token_hint=id_token,
222223
client_id=None,
223224
post_logout_redirect_uri="http://example.org",
224-
) == (True, ("http://example.org", application))
225+
) == (True, ("http://example.org", application), oidc_tokens.user)
225226
assert validate_logout_request(
226227
request=mock_request_for(oidc_tokens.user),
227228
id_token_hint=id_token,
228229
client_id=client_id,
229230
post_logout_redirect_uri="http://example.org",
230-
) == (ALWAYS_PROMPT, ("http://example.org", application))
231+
) == (ALWAYS_PROMPT, ("http://example.org", application), oidc_tokens.user)
231232
with pytest.raises(ClientIdMissmatch):
232233
validate_logout_request(
233234
request=mock_request_for(oidc_tokens.user),
@@ -519,6 +520,47 @@ def test_token_deletion_on_logout(oidc_tokens, loggend_in_client, rp_settings):
519520
assert all([token.revoked <= timezone.now() for token in RefreshToken.objects.all()])
520521

521522

523+
@pytest.mark.django_db
524+
def test_token_deletion_on_logout_expired_session(oidc_tokens, client, rp_settings):
525+
AccessToken = get_access_token_model()
526+
IDToken = get_id_token_model()
527+
RefreshToken = get_refresh_token_model()
528+
assert AccessToken.objects.count() == 1
529+
assert IDToken.objects.count() == 1
530+
assert RefreshToken.objects.count() == 1
531+
rsp = client.get(
532+
reverse("oauth2_provider:rp-initiated-logout"),
533+
data={
534+
"id_token_hint": oidc_tokens.id_token,
535+
"client_id": oidc_tokens.application.client_id,
536+
},
537+
)
538+
assert rsp.status_code == 200
539+
assert not is_logged_in(client)
540+
# Check that all tokens are active.
541+
access_token = AccessToken.objects.get()
542+
assert not access_token.is_expired()
543+
id_token = IDToken.objects.get()
544+
assert not id_token.is_expired()
545+
refresh_token = RefreshToken.objects.get()
546+
assert refresh_token.revoked is None
547+
548+
rsp = client.post(
549+
reverse("oauth2_provider:rp-initiated-logout"),
550+
data={
551+
"id_token_hint": oidc_tokens.id_token,
552+
"client_id": oidc_tokens.application.client_id,
553+
"allow": True,
554+
},
555+
)
556+
assertRedirects(rsp, "http://testserver/", fetch_redirect_response=False)
557+
assert not is_logged_in(client)
558+
# Check that all tokens have either been deleted or expired.
559+
assert all(token.is_expired() for token in AccessToken.objects.all())
560+
assert all(token.is_expired() for token in IDToken.objects.all())
561+
assert all(token.revoked <= timezone.now() for token in RefreshToken.objects.all())
562+
563+
522564
@pytest.mark.django_db
523565
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS)
524566
def test_token_deletion_on_logout_disabled(oidc_tokens, loggend_in_client, rp_settings):

0 commit comments

Comments
 (0)