Skip to content

Commit a2b7bee

Browse files
authored
Clear expired ID tokens from database (#1223)
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 a2b7bee

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
class SampleRefreshToken(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
assert IDToken.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+
def test_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+
assert IDToken.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+
assert IDToken.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+
assert IDToken.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+
assert not IDToken.objects.filter(jti=id_token.jti).exists()
502+
503+
465504
@pytest.mark.django_db
466505
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW)
467506
def test_application_key(oauth2_settings, application):

0 commit comments

Comments
 (0)