Skip to content

web/admin: handle non-string values in formatUUID to prevent Event Log crash (cherry-pick #20804 to version-2025.12)#21051

Merged
BeryJu merged 1 commit intoversion-2025.12from
cherry-pick/20804-to-version-2025.12
Mar 20, 2026
Merged

web/admin: handle non-string values in formatUUID to prevent Event Log crash (cherry-pick #20804 to version-2025.12)#21051
BeryJu merged 1 commit intoversion-2025.12from
cherry-pick/20804-to-version-2025.12

Conversation

@authentik-automation
Copy link
Contributor

Cherry-pick of #20804 to version-2025.12 branch.

Original PR: #20804
Original Author: @tysoncung
Cherry-picked commit: 82111d7

…g crash (#20804)

fix(web): handle non-string values in formatUUID to prevent Event Log crash

When event context contains a device with a non-string pk value,
formatUUID crashes with TypeError: s.substring is not a function,
preventing the entire Event Log page from loading.

Add a type guard to coerce non-string values to their string
representation instead of crashing.

Fixes #20803
@codecov
Copy link

codecov bot commented Mar 20, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2892 1 2891 4
View the top 1 failed test(s) by shortest run time
tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit::test_authorization_consent_implied
Stack Traces | 18.4s run time
self = <unittest.case._Outcome object at 0x7fe0cf75b6f0>
test_case = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.13.12............/x64/lib/python3.13/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
result = <TestCaseFunction test_authorization_consent_implied>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.13.12............/x64/lib/python3.13/unittest/case.py:651: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
method = <bound method TestProviderOAuth2OIDCImplicit.test_authorization_consent_implied of <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.13.12............/x64/lib/python3.13/unittest/case.py:606: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
args = (), kwargs = {}

    @wraps(func)
    def wrapper(self: TransactionTestCase, *args, **kwargs):
        """Run test again if we're below max_retries, including tearDown and
        setUp. Otherwise raise the error"""
        nonlocal count
        try:
>           return func(self, *args, **kwargs)

tests/e2e/utils.py:405: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Invalidation flow\nentries:\n- attrs:\n    designation: invalidation\n    na...0\n    stage: !KeyOf default-invalidation-logout\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}
file = 'default/flow-default-provider-authorization-implicit-consent.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Provider authorization flow (implicit consent)\nentries:\n- attrs:\n    desi...henticated\n  identifiers:\n    slug: default-provider-authorization-implicit-consent\n  model: authentik_flows.flow\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}, file = 'system/providers-oauth2.yaml'
content = 'version: 1\nmetadata:\n  labels:\n    blueprints.goauthentik.io/system: "true"\n  name: System - OAuth2 Provider - Sc... application the ability to access the authentik API\n        # on behalf of the authorizing user\n        return {}\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto>

    @wraps(func)
    def wrapper(*args, **kwargs):
        config = apps.get_app_config(app_name)
        if isinstance(config, ManagedAppConfig):
            config._on_startup_callback(None)
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
    @apply_blueprint("system/providers-oauth2.yaml")
    @reconcile_app("authentik_crypto")
    def test_authorization_consent_implied(self):
        """test OpenID Provider flow (default authorization flow with implied consent)"""
        sleep(1)
        # Bootstrap all needed objects
        authorization_flow = Flow.objects.get(
            slug="default-provider-authorization-implicit-consent"
        )
        provider = OAuth2Provider.objects.create(
            name=self.application_slug,
            client_type=ClientTypes.CONFIDENTIAL,
            client_id=self.client_id,
            client_secret=self.client_secret,
            signing_key=create_test_cert(),
            redirect_uris=[
                RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
            ],
            authorization_flow=authorization_flow,
        )
        provider.property_mappings.set(
            ScopeMapping.objects.filter(
                scope_name__in=[
                    SCOPE_OPENID,
                    SCOPE_OPENID_EMAIL,
                    SCOPE_OPENID_PROFILE,
                    SCOPE_OFFLINE_ACCESS,
                ]
            )
        )
        provider.save()
        Application.objects.create(
            name=self.application_slug,
            slug=self.application_slug,
            provider=provider,
        )
        self.setup_client()
    
        self.driver.get("http://localhost:9009/implicit/")
        self.wait.until(ec.title_contains("authentik"))
        self.login()
    
>       body = self.parse_json_content()

tests/e2e/test_provider_oidc_implicit.py:153: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
context = None, timeout = 10

    def parse_json_content(
        self, context: WebElement | None = None, timeout: float | None = 10
    ) -> JSONType:
        """
        Parse JSON from a Selenium element's text content.
    
        If `context` is not provided, defaults to the <body> element.
        Raises a clear test failure if the element isn't found, the text doesn't appear
        within `timeout` seconds, or the text is not valid JSON.
        """
        use_body = context is None
        wait_timeout = timeout or self.wait_timeout
    
        def get_context() -> WebElement:
            """Get or refresh the context element."""
            if use_body:
                return self.driver.find_element(By.TAG_NAME, "body")
            return context
    
        def get_text_safely() -> str:
            """Get element text, re-finding element if stale."""
            for _ in range(5):
                try:
                    return get_context().text.strip()
                except StaleElementReferenceException:
                    sleep(0.5)
            return get_context().text.strip()
    
        def get_inner_html_safely() -> str:
            """Get innerHTML, re-finding element if stale."""
            for _ in range(5):
                try:
                    return get_context().get_attribute("innerHTML") or ""
                except StaleElementReferenceException:
                    sleep(0.5)
            return get_context().get_attribute("innerHTML") or ""
    
        try:
            get_context()
        except NoSuchElementException:
            self.fail(
                f"No element found (defaulted to <body>). Current URL: {self.driver.current_url}"
            )
    
        wait = WebDriverWait(self.driver, wait_timeout)
    
        try:
            wait.until(lambda d: len(get_text_safely()) != 0)
        except TimeoutException:
            snippet = get_text_safely()[:500].replace("\n", " ")
            self.fail(
                f"Timed out waiting for element text to appear at {self.driver.current_url}. "
                f"Current content: {snippet or '<empty>'}"
            )
    
        body_text = get_text_safely()
        inner_html = get_inner_html_safely()
    
        if "redirecting" in inner_html.lower():
            try:
                wait.until(lambda d: "redirecting" not in get_inner_html_safely().lower())
            except TimeoutException:
                snippet = get_text_safely()[:500].replace("\n", " ")
                inner_html = get_inner_html_safely()
    
                self.fail(
                    f"Timed out waiting for redirect to finish at {self.driver.current_url}. "
                    f"Current content: {snippet or '<empty>'}"
                    f"{inner_html or '<empty>'}"
                )
    
            inner_html = get_inner_html_safely()
            body_text = get_text_safely()
    
        snippet = body_text[:500].replace("\n", " ")
    
        if not body_text.startswith("{") and not body_text.startswith("["):
>           self.fail(
                f"Expected JSON content but got non-JSON text at {self.driver.current_url}: "
                f"{snippet or '<empty>'}"
                f"{inner_html or '<empty>'}"
            )

tests/e2e/utils.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
msg = 'Expected JSON content but got non-JSON text at http://localhost:9009/implicit/#access_token=eyJhbGciOiJSUzI1NiIsImtpZ...en_type=Bearer&expires_in=3600&state=7fa87fa17a2d42e18df4193616d3c535: <empty>\n    <pre id="loginResult"></pre>\n\n\n'

    def fail(self, msg=None):
        """Fail immediately, with the given message."""
>       raise self.failureException(msg)
E       AssertionError: Expected JSON content but got non-JSON text at http://localhost:9009/implicit/#access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4N2NhMGM0MTU3MzNhMDVkMGIwMzA0ZDNkMWQ1YjZmIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwOi8vMTAuMS4wLjI0MDo1NzU2MS9hcHBsaWNhdGlvbi9vL3Rlc3QvIiwic3ViIjoiMzUwMTkyYTMzNDJkYTA0MjViOWNmOTEwMGQwOWVjZjM0ZDU1OTExODMxNjk3YzQ0NmY2YjFmMDY4NDcxNjFlZCIsImF1ZCI6ImlHTUJnV3lwdm9aekd2azJGdE1CbGpiZ1Y2b2ZxVWx1M3JWU1hXVEsiLCJleHAiOjE3NzQwNDQ4NzEsImlhdCI6MTc3NDA0MTI3MSwiYXV0aF90aW1lIjoxNzc0MDQxMjcxLCJhY3IiOiJnb2F1dGhlbnRpay5pby9wcm92aWRlcnMvb2F1dGgyL2RlZmF1bHQiLCJhbXIiOlsicHdkIl0sIm5vbmNlIjoiMTk5ZGJiNTAxZDg4NGZkNDhlNTljOGEwMGRjM2YyNGEiLCJzaWQiOiI1NTM0NjhhYmExYTBmOThjYTFhMjI2MDk3ZDViYzRiODdmMjJhNjA5MDQyOTcyZTMyNjA0NGZjZjBiNDg4ZGUwIiwiZW1haWwiOiJBTUczRkh3dlNmZUNYTW05TG5JREBnb2F1dGhlbnRpay5pbyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkFNRzNGSHd2U2ZlQ1hNbTlMbklEIiwiZ2l2ZW5fbmFtZSI6IkFNRzNGSHd2U2ZlQ1hNbTlMbklEIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQU1HM0ZId3ZTZmVDWE1tOUxuSUQiLCJuaWNrbmFtZSI6IkFNRzNGSHd2U2ZlQ1hNbTlMbklEIiwiZ3JvdXBzIjpbIkFNRzNGSHd2U2ZlQ1hNbTlMbklEIl0sImF6cCI6ImlHTUJnV3lwdm9aekd2azJGdE1CbGpiZ1Y2b2ZxVWx1M3JWU1hXVEsiLCJ1aWQiOiJ4WWxtRjdwd0lqV1FwelFlRmtaYUZiak1jTU16bElGeEthTXVIYU4yIiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIG9wZW5pZCJ9.QgSxfFaXF1km_wINqPhgrtS47reZjyFWksb1QL86rChqNlzYrx8IvNse7GGGDYFV9MHjfDgSTI6mC_fjy2GBpn0dW7gvzTjMJ6Ta6ov4I1uDfk5gUWdEP3gdZQjoRt79Pz2hSbFitH47YWwO4HUs3q4qvMB2_JvY4fznNMdd7H9feo-mAIVWc-CnUHOSv4fCXO9buAsyflhJjW4VMe1miQpNHgVmvQXLDnfYoISVA05PBM6o6Iv-M6q8JKqaAlyRQHtwiYtfxt6X_MrdlP-3Lac_-dSjyOVyMp28_1UUx4z5PNVGrBbwibjzM36zLwRqNjeDVYDyGpGoAT-mHXDr4hu8cnD9-suP-e9vNE5GpZUBMlf-Sj4bU1MKpt5iOvpJ6hySrq1Ttzf4Dtjx0HirqbDOcNgpvRwyzhDlwLBoiekHa0RRw67T_jSCTg2ETYzmp2oine_SYPk-RCCp456KIjEnhCPlxRT9ZNM01Ei3B6S3gwkQOFCngc1JPDy5wD_krp4RWuf0B0-s7SKeOMtTr5Iw45CqmgMi3fyN_7Fs3r4E6EyZwH1rNmTRXNCyE8aalR_cXREu4hOjJrYxNDlJNXlQVy0iZsS6rGIaY48GipW5HjTqVWCBQHtNfaj2eRIlaGyOFgExnLIWv3TNJnHrrZvQB1OW2dfc5yiDiCK8zOY&id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4N2NhMGM0MTU3MzNhMDVkMGIwMzA0ZDNkMWQ1YjZmIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwOi8vMTAuMS4wLjI0MDo1NzU2MS9hcHBsaWNhdGlvbi9vL3Rlc3QvIiwic3ViIjoiMzUwMTkyYTMzNDJkYTA0MjViOWNmOTEwMGQwOWVjZjM0ZDU1OTExODMxNjk3YzQ0NmY2YjFmMDY4NDcxNjFlZCIsImF1ZCI6ImlHTUJnV3lwdm9aekd2azJGdE1CbGpiZ1Y2b2ZxVWx1M3JWU1hXVEsiLCJleHAiOjE3NzQwNDQ4NzEsImlhdCI6MTc3NDA0MTI3MSwiYXV0aF90aW1lIjoxNzc0MDQxMjcxLCJhY3IiOiJnb2F1dGhlbnRpay5pby9wcm92aWRlcnMvb2F1dGgyL2RlZmF1bHQiLCJhbXIiOlsicHdkIl0sIm5vbmNlIjoiMTk5ZGJiNTAxZDg4NGZkNDhlNTljOGEwMGRjM2YyNGEiLCJhdF9oYXNoIjoiUWk5M0ViOGptWXU2all1eTI1YWtkQSIsInNpZCI6IjU1MzQ2OGFiYTFhMGY5OGNhMWEyMjYwOTdkNWJjNGI4N2YyMmE2MDkwNDI5NzJlMzI2MDQ0ZmNmMGI0ODhkZTAiLCJlbWFpbCI6IkFNRzNGSHd2U2ZlQ1hNbTlMbklEQGdvYXV0aGVudGlrLmlvIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiQU1HM0ZId3ZTZmVDWE1tOUxuSUQiLCJnaXZlbl9uYW1lIjoiQU1HM0ZId3ZTZmVDWE1tOUxuSUQiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJBTUczRkh3dlNmZUNYTW05TG5JRCIsIm5pY2tuYW1lIjoiQU1HM0ZId3ZTZmVDWE1tOUxuSUQiLCJncm91cHMiOlsiQU1HM0ZId3ZTZmVDWE1tOUxuSUQiXX0.bZs8qinUGGjOYRBIyyixJ2VpJsKnvZX68SILAgLD9xIXInIGwr0_RFM3321AuuMhKe6S17tkqkJ5T0FnSJqtTUQMB5RJoT0Y3aynUEncWFoL9MOWkTgCrZBNkSvzO6Jr43RH8GpbIY3CfzXJVoe0zX_RnZlz1SXQpH4d85EPkZaY7sntDV8zWDRkwsrjexO-MB44cpR8Bus3k76v4W0FCJmaQnW6NxLvrZX05ZIGGHCDeig-VU0R-OSpRqwApZbAVJy2K65mUyfyY_1oifC2RFidc2n5Rg8WLS4O_ivcuJ6s3WaUyPcouTcl3Hl8vcY17XaLj0hBwgYHOuSqRrDxCfgoBSGolZWvuov_LMtGUy_FziY2V7je2kCr80xySUNlOhwZqj4XPLkCPlJfzoFP-XH0EBWWionquGxq2aEj7tkg4TkJ2JCjeyRFv6N7E8alPMtuY2ouekkCy6Dd5NGrMTv0u8r6RAkm5EOqlsbmjGsjowOA_KhjrNmenVsmLt8E6DqzrcedD90JqxvFMrOxFn7ku6fZ6fYv_vhd1aAo3_JWmz8TeNM9MkPhtcV2DoxZSDTi8_8wE0raFgW4-5KCWnRtHgXznHJjivUOt9ZtlKrXbcmuOBAPVUPaRwCSKcSxZBechLT21fz2pHIuIqqKqQOlXwpyfaDhtC_yKQO-fX0&token_type=Bearer&expires_in=3600&state=7fa87fa17a2d42e18df4193616d3c535: <empty>
E           <pre id="loginResult"></pre>

.../hostedtoolcache/Python/3.13.12............/x64/lib/python3.13/unittest/case.py:732: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@netlify
Copy link

netlify bot commented Mar 20, 2026

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit 2aa3f20
🔍 Latest deploy log https://app.netlify.com/projects/authentik-docs/deploys/69bdb6cd915dee00089b8a55
😎 Deploy Preview https://deploy-preview-21051--authentik-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Mar 20, 2026

Deploy Preview for authentik-integrations ready!

Name Link
🔨 Latest commit 2aa3f20
🔍 Latest deploy log https://app.netlify.com/projects/authentik-integrations/deploys/69bdb6cd74f0480008f16a58
😎 Deploy Preview https://deploy-preview-21051--authentik-integrations.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@BeryJu BeryJu merged commit bd3d4cc into version-2025.12 Mar 20, 2026
72 of 84 checks passed
@BeryJu BeryJu deleted the cherry-pick/20804-to-version-2025.12 branch March 20, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants