Skip to content

Commit 371f091

Browse files
Qup42dopry
authored andcommitted
Add configuration to delete AccessTokens on Logout
1 parent 67a5208 commit 371f091

File tree

5 files changed

+97
-2
lines changed

5 files changed

+97
-2
lines changed

‎docs/settings.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,14 @@ Default: ``True``
334334

335335
Whether expired ID tokens are accepted for RP-Initiated Logout. The Tokens must still be signed by the OP and otherwise valid.
336336

337+
OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS
338+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
339+
Default: ``True``
340+
341+
Whether to delete the access, refresh and ID tokens of the user that is being logged out.
342+
The types of applications for which tokens are deleted can be customized with `RPInitiatedLogoutView.token_types_to_delete`.
343+
The default is to delete the tokens of all applications if this flag is enabled.
344+
337345
OIDC_ISS_ENDPOINT
338346
~~~~~~~~~~~~~~~~~
339347
Default: ``""``

‎oauth2_provider/settings.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"OIDC_RP_INITIATED_LOGOUT_ENABLED": False,
9292
"OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True,
9393
"OIDC_RP_INITIATED_LOGOUT_ACCEPT_EXPIRED_TOKENS": True,
94+
"OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS": True,
9495
# Special settings that will be evaluated at runtime
9596
"_SCOPES": [],
9697
"_DEFAULT_SCOPES": [],

‎oauth2_provider/views/oidc.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@
2222
)
2323
from ..formsimportConfirmLogoutForm
2424
from ..httpimportOAuth2ResponseRedirect
25-
from ..modelsimportget_application_model, get_id_token_model
25+
from ..modelsimport (
26+
get_access_token_model,
27+
get_application_model,
28+
get_id_token_model,
29+
get_refresh_token_model,
30+
)
2631
from ..settingsimportoauth2_settings
2732
from .mixinsimportOAuthLibMixin, OIDCLogoutOnlyMixin, OIDCOnlyMixin
2833

@@ -260,6 +265,10 @@ def validate_logout_request(request, id_token_hint, client_id, post_logout_redir
260265
classRPInitiatedLogoutView(OIDCLogoutOnlyMixin, FormView):
261266
template_name="oauth2_provider/logout_confirm.html"
262267
form_class=ConfirmLogoutForm
268+
token_types_to_delete= [
269+
Application.CLIENT_PUBLIC,
270+
Application.CLIENT_CONFIDENTIAL,
271+
]
263272

264273
defget_initial(self):
265274
return{
@@ -330,7 +339,29 @@ def form_valid(self, form):
330339
returnself.error_response(error)
331340

332341
defdo_logout(self, application=None, post_logout_redirect_uri=None, state=None):
342+
# Delete Access Tokens
343+
ifoauth2_settings.OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS:
344+
AccessToken=get_access_token_model()
345+
RefreshToken=get_refresh_token_model()
346+
access_tokens_to_delete=AccessToken.objects.filter(
347+
user=self.request.user, application__client_type__in=self.token_types_to_delete
348+
)
349+
# This queryset has to be evaluated eagerly. The queryset would be empty with lazy evaluation
350+
# because `access_tokens_to_delete` represents an empty queryset once `refresh_tokens_to_delete`
351+
# is evaluated as all AccessTokens have been deleted.
352+
refresh_tokens_to_delete=list(
353+
RefreshToken.objects.filter(access_token__in=access_tokens_to_delete)
354+
)
355+
fortokeninaccess_tokens_to_delete:
356+
# Delete the token and its corresponding refresh and IDTokens.
357+
iftoken.id_token:
358+
token.id_token.revoke()
359+
token.revoke()
360+
forrefresh_tokeninrefresh_tokens_to_delete:
361+
refresh_token.revoke()
362+
# Logout in Django
333363
logout(self.request)
364+
# Redirect
334365
ifpost_logout_redirect_uri:
335366
ifstate:
336367
returnOAuth2ResponseRedirect(

‎tests/presets.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
OIDC_SETTINGS_RP_LOGOUT["OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT"] =False
3434
OIDC_SETTINGS_RP_LOGOUT_DENY_EXPIRED=deepcopy(OIDC_SETTINGS_RP_LOGOUT)
3535
OIDC_SETTINGS_RP_LOGOUT_DENY_EXPIRED["OIDC_RP_INITIATED_LOGOUT_ACCEPT_EXPIRED_TOKENS"] =False
36+
OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS=deepcopy(OIDC_SETTINGS_RP_LOGOUT)
37+
OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS["OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS"] =False
3638
REST_FRAMEWORK_SCOPES={
3739
"SCOPES":{
3840
"read": "Read scope",

‎tests/test_oidc_views.py‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
fromdjango.contrib.auth.modelsimportAnonymousUser
44
fromdjango.testimportRequestFactory, TestCase
55
fromdjango.urlsimportreverse
6+
fromdjango.utilsimporttimezone
67

78
fromoauth2_provider.exceptionsimportClientIdMissmatch, InvalidOIDCClientError, InvalidOIDCRedirectURIError
8-
fromoauth2_provider.modelsimportget_id_token_model
9+
fromoauth2_provider.modelsimportget_access_token_model, get_id_token_model, get_refresh_token_model
910
fromoauth2_provider.oauth2_validatorsimportOAuth2Validator
1011
fromoauth2_provider.settingsimportoauth2_settings
1112
fromoauth2_provider.views.oidcimport_load_id_token, _validate_claims, validate_logout_request
@@ -474,6 +475,58 @@ def test_userinfo_endpoint_bad_token(oidc_tokens, client):
474475
assertrsp.status_code==401
475476

476477

478+
@pytest.mark.django_db
479+
deftest_token_deletion_on_logout(oidc_tokens, loggend_in_client, rp_settings):
480+
AccessToken=get_access_token_model()
481+
IDToken=get_id_token_model()
482+
RefreshToken=get_refresh_token_model()
483+
assertAccessToken.objects.count() ==1
484+
assertIDToken.objects.count() ==1
485+
assertRefreshToken.objects.count() ==1
486+
rsp=loggend_in_client.get(
487+
reverse("oauth2_provider:rp-initiated-logout"),
488+
data={
489+
"id_token_hint": oidc_tokens.id_token,
490+
"client_id": oidc_tokens.application.client_id,
491+
},
492+
)
493+
assertrsp.status_code==302
494+
assertnotis_logged_in(loggend_in_client)
495+
# Check that all tokens have either been deleted or expired.
496+
assertall([token.is_expired() fortokeninAccessToken.objects.all()])
497+
assertall([token.is_expired() fortokeninIDToken.objects.all()])
498+
assertall([token.revoked<=timezone.now() fortokeninRefreshToken.objects.all()])
499+
500+
501+
@pytest.mark.django_db
502+
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS)
503+
deftest_token_deletion_on_logout_disabled(oidc_tokens, loggend_in_client, rp_settings):
504+
rp_settings.OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS=False
505+
506+
AccessToken=get_access_token_model()
507+
IDToken=get_id_token_model()
508+
RefreshToken=get_refresh_token_model()
509+
assertAccessToken.objects.count() ==1
510+
assertIDToken.objects.count() ==1
511+
assertRefreshToken.objects.count() ==1
512+
rsp=loggend_in_client.get(
513+
reverse("oauth2_provider:rp-initiated-logout"),
514+
data={
515+
"id_token_hint": oidc_tokens.id_token,
516+
"client_id": oidc_tokens.application.client_id,
517+
},
518+
)
519+
assertrsp.status_code==302
520+
assertnotis_logged_in(loggend_in_client)
521+
# Check that the tokens have not been expired or deleted.
522+
assertAccessToken.objects.count() ==1
523+
assertnotany([token.is_expired() fortokeninAccessToken.objects.all()])
524+
assertIDToken.objects.count() ==1
525+
assertnotany([token.is_expired() fortokeninIDToken.objects.all()])
526+
assertRefreshToken.objects.count() ==1
527+
assertnotany([token.revokedisnotNonefortokeninRefreshToken.objects.all()])
528+
529+
477530
EXAMPLE_EMAIL="[email protected]"
478531

479532

0 commit comments

Comments
(0)