Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/ol_social_auth/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ Make sure to properly configure the plugin following the links in the above "Con
* Install the plugin in the lms following the installation steps above.
* Verify that you are not logged in on edx-platform.
* Create a new user in your MIT application and verify that a corresponding user is successfully created on the edX platform.

Expired Token Cleanup
---------------------
This plugin includes a scheduled Celery task (``ol_clear_expired_tokens``) that automatically removes expired OAuth2 access tokens, refresh tokens, and grant tokens from the database.

**Behavior:**

* Runs every **Monday at 9:00 AM** (server time) via Celery Beat by default. The schedule can be customized by overriding the ``ol_clear_expired_tokens`` entry in ``CELERY_BEAT_SCHEDULE``.
* Uses django-oauth-toolkit's ``clear_expired()`` to delete tokens that have exceeded the configured expiration threshold.
* Sets ``REFRESH_TOKEN_EXPIRE_SECONDS`` to **30 days** (overriding the edx-platform default of 90 days). Tokens revoked or expired longer than 30 days ago will be cleaned up.

**Note:** If running this plugin for the first time on a database with a large backlog of expired tokens (millions of rows), consider running the ``edx_clear_expired_tokens`` management command manually first to reduce the initial volume before relying on the scheduled task.
23 changes: 23 additions & 0 deletions src/ol_social_auth/ol_social_auth/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""ol_social_auth Django application initialization."""

from django.apps import AppConfig
from edx_django_utils.plugins import PluginSettings
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType


class OLSocialAuthConfig(AppConfig):
name = "ol_social_auth"
verbose_name = "OL Social Auth"

plugin_app = {
PluginSettings.CONFIG: {
ProjectType.LMS: {
SettingsType.COMMON: {
PluginSettings.RELATIVE_PATH: "settings.common",
},
SettingsType.PRODUCTION: {
PluginSettings.RELATIVE_PATH: "settings.production",
},
},
},
}
Empty file.
17 changes: 17 additions & 0 deletions src/ol_social_auth/ol_social_auth/settings/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Common settings for the ol-social-auth plugin."""

from celery.schedules import crontab


def plugin_settings(settings):
"""Settings for the ol-social-auth plugin.""" # noqa: D401
settings.OAUTH2_PROVIDER["REFRESH_TOKEN_EXPIRE_SECONDS"] = (
30 * 24 * 60 * 60 # 30 days
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.

I'd suggest checking with @blarghmatey this time range as well, since we're using service tokens before merging the PR. Since DevOps creates service tokens, I'm not sure what expiry time they set. I want to make sure they will not be deleted automatically.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The OAuth service accounts that we rely on are using client credentials grants so we're not worried about token expiration. Thanks for checking.

)
Comment thread
Anas12091101 marked this conversation as resolved.
# Add ol_clear_expired_tokens to the Celery beat schedule.
if not hasattr(settings, "CELERY_BEAT_SCHEDULE"):
settings.CELERY_BEAT_SCHEDULE = {}
settings.CELERY_BEAT_SCHEDULE["ol_clear_expired_tokens"] = {
"task": "ol_social_auth.tasks.ol_clear_expired_tokens",
"schedule": crontab(hour=9, minute=0, day_of_week="monday"),
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.

Why not midnight?

Suggested change
"schedule": crontab(hour=9, minute=0, day_of_week="monday"),
"schedule": crontab(hour=0, minute=0, day_of_week="monday"),

Copy link
Copy Markdown
Contributor Author

@Anas12091101 Anas12091101 Apr 8, 2026

Choose a reason for hiding this comment

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

I’m not entirely sure, but this task is scheduled for 9 AM in our other applications as well; for example, MITxOnline and MITxPRO

@pdpinch, what are your thoughts on this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it's a task that we care about succeeding, I prefer to do it during business hours, so we have a better chance of noticing a problem and fixing it.

For this task, like this, really, any time would be fine.

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.

I was able to test it manually via Django shell, but I wasn't able to test it via the beat schedule and the task being run in auto mode. Were you able to test it?

Also, Open edX has CELERYBEAT_SCHEDULE in the settings in common.py instead of CELERY_BEAT_SCHEDULE. Could you tell if there was any specific reason for using a new env key rather than using the existing openedx one? And also, could this be the reason the task schedule isn't working?

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.

Good catch. I’ve updated the setting name in dce4d4b and verified that the tasks are running as expected according to the beat schedule.

}
5 changes: 5 additions & 0 deletions src/ol_social_auth/ol_social_auth/settings/production.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Production settings for the ol-social-auth plugin."""


def plugin_settings(settings):
"""Production overrides for ol-social-auth plugin."""
25 changes: 25 additions & 0 deletions src/ol_social_auth/ol_social_auth/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Celery tasks for ol-social-auth plugin."""

import logging

from celery import shared_task
from oauth2_provider.models import clear_expired

log = logging.getLogger(__name__)
oauth2_logger = logging.getLogger("oauth2_provider")


@shared_task(acks_late=True)
def ol_clear_expired_tokens():
"""Clear expired OAuth2 access, refresh, and ID tokens."""
log.info("Starting ol_clear_expired_tokens...")
# Suppress debug-level logs from oauth2_provider during cleanup.
# Its batch_delete debug logs lack the 'userid' field expected by
# Open edX's custom log formatter, causing noisy ValueError tracebacks.
Comment on lines +16 to +18
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.

Can you explain this a little bit more? I am having a hard time getting it. Did we copy it from Open edX?

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.

This wasn't copied from Open edX — it's a workaround for an incompatibility between django-oauth-toolkit and Open edX's custom log formatter.

The problem: clear_expired() internally calls batch_delete(), which emits debug-level log messages like "10000 tokens deleted, X left". Open edX configures a custom log formatter that expects every log record to have a userid field (used for request-scoped user tracking). The oauth2_provider library doesn't add this field to its log records, so every debug message it emits causes a ValueError: Formatting field not found in record: 'userid' — producing a noisy traceback in the logs on every batch deletion.

The fix: Before calling clear_expired(), we temporarily raise the oauth2_provider logger level to INFO, which silences the debug-level batch messages. After the call completes (or fails), we restore the original level. This way the deletion still happens correctly — we just suppress the log messages that would trigger the formatter error.

To reproduce the issue, run the clear_expired() fn without suppressing the logs.

original_level = oauth2_logger.level
oauth2_logger.setLevel(logging.INFO)
try:
clear_expired()
finally:
oauth2_logger.setLevel(original_level)
log.info("Finished ol_clear_expired_tokens.")
6 changes: 5 additions & 1 deletion src/ol_social_auth/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ol-social-auth"
version = "0.2.0"
version = "0.2.1"
description = "An Open edX plugin implementing MIT social auth backend"
authors = [
{name = "MIT Office of Digital Learning"}
Expand All @@ -11,9 +11,13 @@ requires-python = ">=3.11"
keywords = ["Python", "edx"]
dependencies = [
"Django>=4.0",
"django-oauth-toolkit",
"social-auth-core>=4.5.4",
]

[project.entry-points."lms.djangoapp"]
ol_social_auth = "ol_social_auth.apps:OLSocialAuthConfig"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand Down
26 changes: 26 additions & 0 deletions src/ol_social_auth/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[tool:pytest]
pep8maxlinelength = 119
DJANGO_SETTINGS_MODULE = lms.envs.test
addopts = --nomigrations --reuse-db --durations=20
filterwarnings =
default
ignore::xblock.exceptions.FieldDataDeprecationWarning
ignore::pytest.PytestConfigWarning
ignore:No request passed to the backend, unable to rate-limit:UserWarning
ignore:Flags not at the start of the expression:DeprecationWarning
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc':DeprecationWarning
ignore:invalid escape sequence:DeprecationWarning
ignore:`formatargspec` is deprecated since Python 3.5:DeprecationWarning
ignore:the imp module is deprecated in favour of importlib:DeprecationWarning
ignore:"is" with a literal:SyntaxWarning
ignore:defusedxml.lxml is no longer supported:DeprecationWarning
ignore: `np.int` is a deprecated alias for the builtin `int`.:DeprecationWarning
ignore: `np.float` is a deprecated alias for the builtin `float`.:DeprecationWarning
ignore: `np.complex` is a deprecated alias for the builtin `complex`.:DeprecationWarning
ignore: 'etree' is deprecated. Use 'xml.etree.ElementTree' instead.:DeprecationWarning
ignore: defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead.:DeprecationWarning

junit_family = xunit2
norecursedirs = .* *.egg build conf dist node_modules test_root cms/envs lms/envs
python_classes =
python_files = tests.py test_*.py tests_*.py *_tests.py __init__.py
24 changes: 24 additions & 0 deletions src/ol_social_auth/tests/tasks_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Tests for ol_social_auth tasks."""

from ol_social_auth import tasks


def test_ol_clear_expired_tokens(mocker):
"""Test that ol_clear_expired_tokens calls the clear_expired function."""
patched_clear_expired = mocker.patch("ol_social_auth.tasks.clear_expired")

tasks.ol_clear_expired_tokens.delay()
patched_clear_expired.assert_called_once_with()


def test_ol_clear_expired_tokens_logging(mocker):
"""Test that ol_clear_expired_tokens logs start and finish messages."""
mocker.patch("ol_social_auth.tasks.clear_expired")
patched_log_info = mocker.patch("ol_social_auth.tasks.log.info")

tasks.ol_clear_expired_tokens()

expected_log_call_count = 2
assert patched_log_info.call_count == expected_log_call_count # noqa: S101
patched_log_info.assert_any_call("Starting ol_clear_expired_tokens...")
patched_log_info.assert_any_call("Finished ol_clear_expired_tokens.")
Loading