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:
parent
5e5bc5cd49
commit
63c52fd936
12
authentik/sources/oauth/settings.py
Normal file
12
authentik/sources/oauth/settings.py
Normal 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"},
|
||||
},
|
||||
}
|
70
authentik/sources/oauth/tasks.py
Normal file
70
authentik/sources/oauth/tasks.py
Normal 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)
|
48
authentik/sources/oauth/tests/test_tasks.py
Normal file
48
authentik/sources/oauth/tests/test_tasks.py
Normal 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",
|
||||
},
|
||||
)
|
Reference in a new issue