sources/oauth: periodically update OAuth sources' OIDC configuration (#7245)

* sources/oauth: periodically update OAuth sources' OIDC configuration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make monitored task

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-10-20 16:51:37 +02:00 committed by GitHub
parent 5e5bc5cd49
commit 63c52fd936
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 130 additions and 0 deletions

View file

@ -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"},
},
}

View file

@ -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)

View file

@ -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",
},
)