Skip to content

Commit 80f8af2

Browse files
committed
Clear expired ID tokens from database
The `cleartokens` management command removed expired refresh tokens and associated access tokens but kept expired ID tokens in the database. Remove ID tokens when the associated access and refresh tokens are cleared. Preserve expired ID tokens until the associated access token is deleted to keep relationships intact and not trigger delete cascades. Fixes#1222
1 parent e0c2fc8 commit 80f8af2

File tree

7 files changed

+58
-0
lines changed

7 files changed

+58
-0
lines changed

‎AUTHORS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Julien Palard
6262
Jun Zhou
6363
Kaleb Porter
6464
Kristian Rune Larsen
65+
Ludwig Hähne
6566
Matias Seniquiel
6667
Michael Howitz
6768
Owen Gong

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
### Changed
2020
*#1211 documentation improve on 'AUTHORIZATION_CODE_EXPIRE_SECONDS'.
2121
*#1218 Confim support for Python 3.11.
22+
*#1222 Remove expired ID tokens alongside access tokens in `cleartokens` management command
2223

2324
## [2.2.0] 2022-10-18
2425

‎docs/management_commands.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ problem since refresh tokens are long lived.
2222
To prevent the CPU and RAM high peaks during deletion process use ``CLEAR_EXPIRED_TOKENS_BATCH_SIZE`` and
2323
``CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL`` settings to adjust the process speed.
2424

25+
The ``cleartokens`` management command will also delete expired access and ID tokens alongside expired refresh tokens.
26+
2527
Note: Refresh tokens need to expire before AccessTokens can be removed from the
2628
database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect.
2729

‎oauth2_provider/models.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ def batch_delete(queryset, query):
663663
refresh_expire_at=None
664664
access_token_model=get_access_token_model()
665665
refresh_token_model=get_refresh_token_model()
666+
id_token_model=get_id_token_model()
666667
grant_model=get_grant_model()
667668
REFRESH_TOKEN_EXPIRE_SECONDS=oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS
668669

@@ -696,6 +697,12 @@ def batch_delete(queryset, query):
696697
access_tokens_delete_no=batch_delete(access_tokens, access_token_query)
697698
logger.info("%s Expired access tokens deleted", access_tokens_delete_no)
698699

700+
id_token_query=models.Q(access_token__isnull=True, expires__lt=now)
701+
id_tokens=id_token_model.objects.filter(id_token_query)
702+
703+
id_tokens_delete_no=batch_delete(id_tokens, id_token_query)
704+
logger.info("%s Expired ID tokens deleted", id_tokens_delete_no)
705+
699706
grants_query=models.Q(expires__lt=now)
700707
grants=grant_model.objects.filter(grants_query)
701708

‎tests/models.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class SampleAccessToken(AbstractAccessToken):
3232
null=True,
3333
related_name="s_refreshed_access_token",
3434
)
35+
id_token=models.OneToOneField(
36+
oauth2_settings.ID_TOKEN_MODEL,
37+
on_delete=models.CASCADE,
38+
blank=True,
39+
null=True,
40+
related_name="s_access_token",
41+
)
3542

3643

3744
classSampleRefreshToken(AbstractRefreshToken):

‎tests/presets.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
},
2121
"DEFAULT_SCOPES": ["read", "write"],
2222
"PKCE_REQUIRED": False,
23+
"REFRESH_TOKEN_EXPIRE_SECONDS": 3600,
2324
}
2425
OIDC_SETTINGS_RO=deepcopy(OIDC_SETTINGS_RW)
2526
OIDC_SETTINGS_RO["DEFAULT_SCOPES"] = ["read"]

‎tests/test_models.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,45 @@ def test_id_token_methods(oidc_tokens, rf):
462462
assertIDToken.objects.filter(jti=id_token.jti).count() ==0
463463

464464

465+
@pytest.mark.django_db
466+
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW)
467+
deftest_clear_expired_id_tokens(oauth2_settings, oidc_tokens, rf):
468+
id_token=IDToken.objects.get()
469+
access_token=id_token.access_token
470+
471+
# All tokens still valid
472+
clear_expired()
473+
474+
assertIDToken.objects.filter(jti=id_token.jti).exists()
475+
476+
earlier=timezone.now() -timedelta(minutes=1)
477+
id_token.expires=earlier
478+
id_token.save()
479+
480+
# ID token should be preserved until the access token is deleted
481+
clear_expired()
482+
483+
assertIDToken.objects.filter(jti=id_token.jti).exists()
484+
485+
access_token.expires=earlier
486+
access_token.save()
487+
488+
# ID and access tokens are expired but refresh token is still valid
489+
clear_expired()
490+
491+
assertIDToken.objects.filter(jti=id_token.jti).exists()
492+
493+
# Mark refresh token as expired
494+
delta=timedelta(seconds=oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS+60)
495+
access_token.expires=timezone.now() -delta
496+
access_token.save()
497+
498+
# With the refresh token expired, the ID token should be deleted
499+
clear_expired()
500+
501+
assertnotIDToken.objects.filter(jti=id_token.jti).exists()
502+
503+
465504
@pytest.mark.django_db
466505
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW)
467506
deftest_application_key(oauth2_settings, application):

0 commit comments

Comments
(0)