Skip to content

Commit 09e745d

Browse files
Qup42dopry
authored andcommitted
Add configuration to delete AccessTokens on Logout
1 parent c98fa0b commit 09e745d

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 ..forms import ConfirmLogoutForm
2424
from ..http import OAuth2ResponseRedirect
25-
from ..models import get_application_model, get_id_token_model
25+
from ..models import (
26+
get_access_token_model,
27+
get_application_model,
28+
get_id_token_model,
29+
get_refresh_token_model,
30+
)
2631
from ..settings import oauth2_settings
2732
from .mixins import OAuthLibMixin, OIDCLogoutOnlyMixin, OIDCOnlyMixin
2833

@@ -260,6 +265,10 @@ def validate_logout_request(request, id_token_hint, client_id, post_logout_redir
260265
class RPInitiatedLogoutView(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
def get_initial(self):
265274
return {
@@ -330,7 +339,29 @@ def form_valid(self, form):
330339
return self.error_response(error)
331340

332341
def do_logout(self, application=None, post_logout_redirect_uri=None, state=None):
342+
# Delete Access Tokens
343+
if oauth2_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+
for token in access_tokens_to_delete:
356+
# Delete the token and its corresponding refresh and IDTokens.
357+
if token.id_token:
358+
token.id_token.revoke()
359+
token.revoke()
360+
for refresh_token in refresh_tokens_to_delete:
361+
refresh_token.revoke()
362+
# Logout in Django
333363
logout(self.request)
364+
# Redirect
334365
if post_logout_redirect_uri:
335366
if state:
336367
return OAuth2ResponseRedirect(

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
from django.contrib.auth.models import AnonymousUser
44
from django.test import RequestFactory, TestCase
55
from django.urls import reverse
6+
from django.utils import timezone
67

78
from oauth2_provider.exceptions import ClientIdMissmatch, InvalidOIDCClientError, InvalidOIDCRedirectURIError
8-
from oauth2_provider.models import get_id_token_model
9+
from oauth2_provider.models import get_access_token_model, get_id_token_model, get_refresh_token_model
910
from oauth2_provider.oauth2_validators import OAuth2Validator
1011
from oauth2_provider.settings import oauth2_settings
1112
from oauth2_provider.views.oidc import _load_id_token, _validate_claims, validate_logout_request
@@ -474,6 +475,58 @@ def test_userinfo_endpoint_bad_token(oidc_tokens, client):
474475
assert rsp.status_code == 401
475476

476477

478+
@pytest.mark.django_db
479+
def test_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+
assert AccessToken.objects.count() == 1
484+
assert IDToken.objects.count() == 1
485+
assert RefreshToken.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+
assert rsp.status_code == 302
494+
assert not is_logged_in(loggend_in_client)
495+
# Check that all tokens have either been deleted or expired.
496+
assert all([token.is_expired() for token in AccessToken.objects.all()])
497+
assert all([token.is_expired() for token in IDToken.objects.all()])
498+
assert all([token.revoked <= timezone.now() for token in RefreshToken.objects.all()])
499+
500+
501+
@pytest.mark.django_db
502+
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS)
503+
def test_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+
assert AccessToken.objects.count() == 1
510+
assert IDToken.objects.count() == 1
511+
assert RefreshToken.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+
assert rsp.status_code == 302
520+
assert not is_logged_in(loggend_in_client)
521+
# Check that the tokens have not been expired or deleted.
522+
assert AccessToken.objects.count() == 1
523+
assert not any([token.is_expired() for token in AccessToken.objects.all()])
524+
assert IDToken.objects.count() == 1
525+
assert not any([token.is_expired() for token in IDToken.objects.all()])
526+
assert RefreshToken.objects.count() == 1
527+
assert not any([token.revoked is not None for token in RefreshToken.objects.all()])
528+
529+
477530
EXAMPLE_EMAIL = "[email protected]"
478531

479532

0 commit comments

Comments
 (0)