From 63c52fd936ed6f4615817a596e63188b8e70c727 Mon Sep 17 00:00:00 2001 From: Jens L Date: Fri, 20 Oct 2023 16:51:37 +0200 Subject: [PATCH] sources/oauth: periodically update OAuth sources' OIDC configuration (#7245) * sources/oauth: periodically update OAuth sources' OIDC configuration Signed-off-by: Jens Langhammer * make monitored task Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- authentik/sources/oauth/settings.py | 12 ++++ authentik/sources/oauth/tasks.py | 70 +++++++++++++++++++++ authentik/sources/oauth/tests/test_tasks.py | 48 ++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 authentik/sources/oauth/settings.py create mode 100644 authentik/sources/oauth/tasks.py create mode 100644 authentik/sources/oauth/tests/test_tasks.py diff --git a/authentik/sources/oauth/settings.py b/authentik/sources/oauth/settings.py new file mode 100644 index 000000000..c839c338d --- /dev/null +++ b/authentik/sources/oauth/settings.py @@ -0,0 +1,12 @@ +"""OAuth source settings""" +from celery.schedules import crontab + +from authentik.lib.utils.time import fqdn_rand + +CELERY_BEAT_SCHEDULE = { + "update_oauth_source_oidc_well_known": { + "task": "authentik.sources.oauth.tasks.update_well_known_jwks", + "schedule": crontab(minute=fqdn_rand("update_well_known_jwks"), hour="*/3"), + "options": {"queue": "authentik_scheduled"}, + }, +} diff --git a/authentik/sources/oauth/tasks.py b/authentik/sources/oauth/tasks.py new file mode 100644 index 000000000..1588117b9 --- /dev/null +++ b/authentik/sources/oauth/tasks.py @@ -0,0 +1,70 @@ +"""OAuth Source tasks""" +from json import dumps + +from requests import RequestException +from structlog.stdlib import get_logger + +from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus +from authentik.lib.utils.http import get_http_session +from authentik.root.celery import CELERY_APP +from authentik.sources.oauth.models import OAuthSource + +LOGGER = get_logger() + + +@CELERY_APP.task(bind=True, base=MonitoredTask) +def update_well_known_jwks(self: MonitoredTask): + """Update OAuth sources' config from well_known, and JWKS info from the configured URL""" + session = get_http_session() + result = TaskResult(TaskResultStatus.SUCCESSFUL, []) + for source in OAuthSource.objects.all().exclude(oidc_well_known_url=""): + try: + well_known_config = session.get(source.oidc_well_known_url) + well_known_config.raise_for_status() + except RequestException as exc: + text = exc.response.text if exc.response else str(exc) + LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text) + result.messages.append(f"Failed to update OIDC configuration for {source.slug}") + continue + config = well_known_config.json() + try: + dirty = False + source_attr_key = ( + ("authorization_url", "authorization_endpoint"), + ("access_token_url", "token_endpoint"), + ("profile_url", "userinfo_endpoint"), + ("oidc_jwks_url", "jwks_uri"), + ) + for source_attr, config_key in source_attr_key: + # Check if we're actually changing anything to only + # save when something has changed + if getattr(source, source_attr) != config[config_key]: + dirty = True + setattr(source, source_attr, config[config_key]) + except (IndexError, KeyError) as exc: + LOGGER.warning( + "Failed to update well_known", + source=source, + exc=exc, + ) + result.messages.append(f"Failed to update OIDC configuration for {source.slug}") + continue + if dirty: + LOGGER.info("Updating sources' OpenID Configuration", source=source) + source.save() + + for source in OAuthSource.objects.all().exclude(oidc_jwks_url=""): + try: + jwks_config = session.get(source.oidc_jwks_url) + jwks_config.raise_for_status() + except RequestException as exc: + text = exc.response.text if exc.response else str(exc) + LOGGER.warning("Failed to update JWKS", source=source, exc=exc, text=text) + result.messages.append(f"Failed to update JWKS for {source.slug}") + continue + config = jwks_config.json() + if dumps(source.oidc_jwks, sort_keys=True) != dumps(config, sort_keys=True): + source.oidc_jwks = config + LOGGER.info("Updating sources' JWKS", source=source) + source.save() + self.set_status(result) diff --git a/authentik/sources/oauth/tests/test_tasks.py b/authentik/sources/oauth/tests/test_tasks.py new file mode 100644 index 000000000..54f1290b6 --- /dev/null +++ b/authentik/sources/oauth/tests/test_tasks.py @@ -0,0 +1,48 @@ +"""Test OAuth Source tasks""" +from django.test import TestCase +from requests_mock import Mocker + +from authentik.sources.oauth.models import OAuthSource +from authentik.sources.oauth.tasks import update_well_known_jwks + + +class TestOAuthSourceTasks(TestCase): + """Test OAuth Source tasks""" + + def setUp(self) -> None: + self.source = OAuthSource.objects.create( + name="test", + slug="test", + provider_type="openidconnect", + authorization_url="", + profile_url="", + consumer_key="", + ) + + @Mocker() + def test_well_known_jwks(self, mock: Mocker): + """Test well_known update""" + self.source.oidc_well_known_url = "http://foo/.well-known/openid-configuration" + self.source.save() + mock.get( + self.source.oidc_well_known_url, + json={ + "authorization_endpoint": "foo", + "token_endpoint": "foo", + "userinfo_endpoint": "foo", + "jwks_uri": "http://foo/jwks", + }, + ) + mock.get("http://foo/jwks", json={"foo": "bar"}) + update_well_known_jwks() # pylint: disable=no-value-for-parameter + self.source.refresh_from_db() + self.assertEqual(self.source.authorization_url, "foo") + self.assertEqual(self.source.access_token_url, "foo") + self.assertEqual(self.source.profile_url, "foo") + self.assertEqual(self.source.oidc_jwks_url, "http://foo/jwks") + self.assertEqual( + self.source.oidc_jwks, + { + "foo": "bar", + }, + )