Skip to content

feat: add clear_expired_tokens task to remove oauth expired tokens#3835

Merged
Anas12091101 merged 8 commits intomasterfrom
anas/add-clear_expired_tokens-task
Mar 12, 2026
Merged

feat: add clear_expired_tokens task to remove oauth expired tokens#3835
Anas12091101 merged 8 commits intomasterfrom
anas/add-clear_expired_tokens-task

Conversation

@Anas12091101
Copy link
Copy Markdown
Contributor

What are the relevant tickets?

https://github.com/mitodl/hq/issues/10115

Description (What does it do?)

This PR adds clear_expired_tokens task to remove OAuth expired tokens. The task is set to run every week.

Screenshots (if appropriate):

  • Desktop screenshots
  • Mobile width screenshots

How can this be tested?

  • Create 1M access and refresh tokens using this management command: seed_expired_tokens.py
  • Run the command ./manage.py cleartokens and verify it deletes only expired tokens

Additional Context

@Anas12091101 Anas12091101 force-pushed the anas/add-clear_expired_tokens-task branch from 46b5174 to a4662cb Compare March 4, 2026 15:09
@Anas12091101 Anas12091101 force-pushed the anas/add-clear_expired_tokens-task branch from 39a54ac to 6af50ce Compare March 4, 2026 18:25
@Anas12091101
Copy link
Copy Markdown
Contributor Author

@dsubak, there are approximately ~1.8M access and refresh tokens in xPRO, and the COUNT(*) query returned almost instantly. Given that, it seems the table size isn’t causing performance issues.

Do you think we still need the chunked_oauth_access_token_delete.py script in this case?

@dsubak
Copy link
Copy Markdown

dsubak commented Mar 5, 2026

@dsubak, there are approximately ~1.8M access and refresh tokens in xPRO, and the COUNT(*) query returned almost instantly. Given that, it seems the table size isn’t causing performance issues.

Do you think we still need the chunked_oauth_access_token_delete.py script in this case?

@Anas12091101 I'd give a manual run of cleartokens a shot first. No need to use my weird script unless it doesn't succeed!

@arslanashraf7 arslanashraf7 self-assigned this Mar 6, 2026
Comment thread mitxpro/tasks.py Outdated
log = logging.getLogger(__name__)


@app.task(acks_late=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a shared task; it doesn't need to depend on an app.

Comment thread mitxpro/settings.py Outdated
Comment on lines +774 to +784
CRON_CLEAR_TOKENS_HOURS = get_string(
name="CRON_CLEAR_TOKENS_HOURS",
default=0,
description="'hours' value for the 'clear-expired-tokens' scheduled task (defaults to midnight)",
)
CRON_CLEAR_TOKENS_DAYS = get_string(
name="CRON_CLEAR_TOKENS_DAYS",
default="*",
description="'day_of_month' value for 'clear-expired-tokens' scheduled task (default will run every day)",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more like a one-off setting, and I don't think we are going to tweak it often. We should probably remove these settings.

Comment thread mitxpro/settings.py
Comment on lines +914 to +923
"clear-expired-tokens": {
"task": "mitxpro.tasks.clear_expired_tokens",
"schedule": crontab(
minute=0,
hour=CRON_CLEAR_TOKENS_HOURS,
day_of_week="*",
day_of_month=CRON_CLEAR_TOKENS_DAYS,
month_of_year="*",
),
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a decision on running it daily?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configurations can be shortened depending on the decision. Daily sounds too often for this task.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arslanashraf7 I’ve configured it similar to MITxOnline, so the task will run every Monday at 9 AM.

Comment thread app.json
Comment on lines +556 to +559
"REFRESH_TOKEN_EXPIRE_SECONDS": {
"description": "Number of seconds until a refresh token expires",
"required": false
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you run generate_app_json command for this?

Comment thread mitxpro/tasks.py Outdated
def clear_expired_tokens():
"""Clear expired OAuth2 access, refresh, and ID tokens via the cleartokens management command."""
log.info("Starting clear_expired_tokens...")
call_command("cleartokens")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In mitodl/mitxonline#3248, we switched from the command to the direct method from oauth2_provider.models import clear_expired. Perhaps we should do the same.

Comment thread mitxpro/tasks_test.py Outdated
Comment on lines +23 to +65
def test_clear_tokens_does_not_delete_unexpired_tokens(settings):
"""Test that cleartokens does not delete tokens that have not yet expired"""
settings.OAUTH2_PROVIDER = {
**settings.OAUTH2_PROVIDER,
"REFRESH_TOKEN_EXPIRE_SECONDS": 60 * 60 * 24 * 30, # 30 days
}

user = UserFactory.create()
application = Application.objects.create(
name="test-cleartokens-app",
client_type="confidential",
authorization_grant_type="authorization-code",
skip_authorization=True,
)

now = timezone.now()

# Create an unexpired access token (expires 1 hour from now)
access_token = AccessToken.objects.create(
user=user,
application=application,
token=uuid.uuid4().hex,
expires=now + timedelta(hours=1),
scope="read write",
)

# Create a non-revoked refresh token
refresh_token = RefreshToken.objects.create(
user=user,
application=application,
token=uuid.uuid4().hex,
access_token=access_token,
)

tasks.clear_expired_tokens()

# Unexpired tokens should still exist
assert AccessToken.objects.filter(pk=access_token.pk).exists()
assert RefreshToken.objects.filter(pk=refresh_token.pk).exists()


@pytest.mark.django_db
def test_clear_tokens_deletes_expired_tokens(settings):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these could be converted into a single parameterized test. This will also reduce the code by a large extent.

Copy link
Copy Markdown
Contributor

@arslanashraf7 arslanashraf7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍. One small change suggested in tests.

Comment thread mitxpro/tasks_test.py
Comment on lines +25 to +26
pytest.param(timedelta(hours=1), None, False, id="unexpired"),
pytest.param(timedelta(days=-60), timedelta(days=-60), True, id="expired"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add more params for safe side:

  • one where we give both token_expires_delta and token_revoked_delta values in positive
  • one where we give both token_expires_delta as negative and token_revoked_delta as None

@Anas12091101 Anas12091101 force-pushed the anas/add-clear_expired_tokens-task branch from 8295c3e to db9f343 Compare March 12, 2026 14:50
@Anas12091101 Anas12091101 merged commit 91fe1f3 into master Mar 12, 2026
9 checks passed
@Anas12091101 Anas12091101 deleted the anas/add-clear_expired_tokens-task branch March 12, 2026 17:21
This was referenced Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants