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