Skip to content

providers/oauth2: fix session decode when upgrading from 2026.2#22684

Merged
BeryJu merged 1 commit into
mainfrom
providers/oauth2/fix-session-code
May 27, 2026
Merged

providers/oauth2: fix session decode when upgrading from 2026.2#22684
BeryJu merged 1 commit into
mainfrom
providers/oauth2/fix-session-code

Conversation

@BeryJu
Copy link
Copy Markdown
Member

@BeryJu BeryJu commented May 27, 2026

closes #22588

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
@BeryJu BeryJu requested a review from a team as a code owner May 27, 2026 10:55
@BeryJu BeryJu added the backport/version-2026.5 Add this label to PRs to backport changes to version-2026.5 label May 27, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented May 27, 2026

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit ed58145
🔍 Latest deploy log https://app.netlify.com/projects/authentik-storybook/deploys/6a16cdb94d6843000841b558
😎 Deploy Preview https://deploy-preview-22684--authentik-storybook.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
Copy Markdown

netlify Bot commented May 27, 2026

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit ed58145
🔍 Latest deploy log https://app.netlify.com/projects/authentik-docs/deploys/6a16cdb93bc5d200081d7c59
😎 Deploy Preview https://deploy-preview-22684--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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
715 2 713 0
View the top 2 failed test(s) by shortest run time
authentik.flows.tests.test_executor.TestFlowExecutor::test_invalid_json
Stack Traces | 67.6s run time
self = <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>
sql = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None
ignored_wrapper_args = (False, {'connection': <DatabaseWrapper vendor='postgresql' alias='default'>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        # Raise a warning during app initialization (stored_app_configs is only
        # ever set during testing).
        if not apps.ready and not apps.stored_app_configs:
            warnings.warn(self.APPS_NOT_READY_WARNING_MSG, category=RuntimeWarning)
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                # params default might be backend specific.
>               return self.cursor.execute(sql)
                       ^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../db/backends/utils.py:103: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django_prometheus.db.common.ExportingCursorWrapper.<locals>.CursorWrapper [closed] [INTRANS] (host=localhost user=authentik database=test_authentik) at 0x7fa571056eb0>
args = ('SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;',)
kwargs = {}

    def execute(self, *args, **kwargs):
        execute_total.labels(alias, vendor).inc()
        with (
            query_duration_seconds.labels(**labels).time(),
            ExceptionCounterByType(errors_total, extra_labels=labels),
        ):
>           return super().execute(*args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../django_prometheus/db/common.py:69: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django_prometheus.db.common.ExportingCursorWrapper.<locals>.CursorWrapper [closed] [INTRANS] (host=localhost user=authentik database=test_authentik) at 0x7fa571056eb0>
query = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None

    def execute(
        self,
        query: Query,
        params: Params | None = None,
        *,
        prepare: bool | None = None,
        binary: bool | None = None,
    ) -> Self:
        """
        Execute a query or command to the database.
        """
        try:
            with self._conn.lock:
                self._conn.wait(
                    self._execute_gen(query, params, prepare=prepare, binary=binary)
                )
        except e._NO_TRACEBACK as ex:
>           raise ex.with_traceback(None)
E           psycopg.errors.UndefinedTable: relation "public.authentik_install_id" does not exist
E           LINE 1: SELECT id FROM public.authentik_install_id ORDER BY id LIMIT...
E                                  ^

.venv/lib/python3.14....../site-packages/psycopg/cursor.py:117: UndefinedTable

The above exception was the direct cause of the following exception:

self = <unittest.case._Outcome object at 0x7fa570e87770>
test_case = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>
subTest = False

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

.../hostedtoolcache/Python/3.14.5............/x64/lib/python3.14/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>
result = <TestCaseFunction test_invalid_json>

    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.14.5............/x64/lib/python3.14/unittest/case.py:669: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>
method = <bound method TestFlowExecutor.test_invalid_json of <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>>

    def _callTestMethod(self, method):
>       result = method()
                 ^^^^^^^^

.../hostedtoolcache/Python/3.14.5............/x64/lib/python3.14/unittest/case.py:615: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>,)
keywargs = {}
newargs = (<authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>,)
newkeywargs = {}

    @wraps(func)
    def patched(*args, **keywargs):
        with self.decoration_helper(patched,
                                    args,
                                    keywargs) as (newargs, newkeywargs):
>           return func(*newargs, **newkeywargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../hostedtoolcache/Python/3.14.5............/x64/lib/python3.14/unittest/mock.py:1439: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.flows.tests.test_executor.TestFlowExecutor testMethod=test_invalid_json>

    @patch(
        "authentik.flows.views.executor.to_stage_response",
        TO_STAGE_RESPONSE_MOCK,
    )
    def test_invalid_json(self):
        """Test invalid JSON body"""
        flow = create_test_flow()
        FlowStageBinding.objects.create(
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
        )
        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
    
        with override_settings(TEST=False, DEBUG=False):
            self.client.logout()
>           response = self.client.post(url, data="{", content_type="application/json")
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../flows/tests/test_executor.py:693: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>
path = '.../flows/executor/u6t52ff6i1/', data = '{', format = None
content_type = 'application/json', follow = False, extra = {}

    def post(self, path, data=None, format=None, content_type=None,
             follow=False, **extra):
>       response = super().post(
            path, data=data, format=format, content_type=content_type, **extra)

.venv/lib/python3.14.............../site-packages/rest_framework/test.py:283: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>
path = '.../flows/executor/u6t52ff6i1/', data = b'{', format = None
content_type = 'application/json', extra = {}

    def post(self, path, data=None, format=None, content_type=None, **extra):
        data, content_type = self._encode_data(data, format, content_type)
>       return self.generic('POST', path, data, content_type, **extra)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/rest_framework/test.py:197: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>, method = 'POST'
path = '.../flows/executor/u6t52ff6i1/', data = b'{'
content_type = 'application/json', secure = False
extra = {'CONTENT_TYPE': 'application/json'}

    def generic(self, method, path, data='',
                content_type='application/octet-stream', secure=False, **extra):
        # Include the CONTENT_TYPE, regardless of whether or not data is empty.
        if content_type is not None:
            extra['CONTENT_TYPE'] = str(content_type)
    
>       return super().generic(
            method, path, data, content_type, secure, **extra)

.venv/lib/python3.14.............../site-packages/rest_framework/test.py:221: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>, method = 'POST'
path = '.../flows/executor/u6t52ff6i1/', data = b'{'
content_type = 'application/json', secure = False, headers = None
query_params = None, extra = {'CONTENT_TYPE': 'application/json'}
parsed = SplitResult(scheme='', netloc='', path='.../flows/executor/u6t52ff6i1/', query='', fragment='')
r = {'CONTENT_LENGTH': '1', 'CONTENT_TYPE': 'application/json', 'PATH_INFO': '.../flows/executor/u6t52ff6i1/', 'QUERY_STRING': '', ...}

    def generic(
        self,
        method,
        path,
        data="",
        content_type="application/octet-stream",
        secure=False,
        *,
        headers=None,
        query_params=None,
        **extra,
    ):
        """Construct an arbitrary HTTP request."""
        parsed = urlsplit(str(path))  # path can be lazy
        data = force_bytes(data, settings.DEFAULT_CHARSET)
        r = {
            "PATH_INFO": self._get_path(parsed),
            "REQUEST_METHOD": method,
            "SERVER_PORT": "443" if secure else "80",
            "wsgi.url_scheme": "https" if secure else "http",
        }
        if data:
            r.update(
                {
                    "CONTENT_LENGTH": str(len(data)),
                    "CONTENT_TYPE": content_type,
                    "wsgi.input": FakePayload(data),
                }
            )
        if headers:
            extra.update(HttpHeaders.to_wsgi_names(headers))
        if query_params:
            extra["QUERY_STRING"] = urlencode(query_params, doseq=True)
        r.update(extra)
        # If QUERY_STRING is absent or empty, we want to extract it from the URL.
        if not r.get("QUERY_STRING"):
            # WSGI requires latin-1 encoded strings. See get_path_info().
            r["QUERY_STRING"] = parsed.query.encode().decode("iso-8859-1")
>       return self.request(**r)
               ^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../django/test/client.py:671: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>
kwargs = {'CONTENT_LENGTH': '1', 'CONTENT_TYPE': 'application/json', 'PATH_INFO': '.../flows/executor/u6t52ff6i1/', 'QUERY_STRING': '', ...}

    def request(self, **kwargs):
        # Ensure that any credentials set get added to every request.
        kwargs.update(self._credentials)
>       return super().request(**kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/rest_framework/test.py:273: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>
kwargs = {'CONTENT_LENGTH': '1', 'CONTENT_TYPE': 'application/json', 'PATH_INFO': '.../flows/executor/u6t52ff6i1/', 'QUERY_STRING': '', ...}

    def request(self, **kwargs):
>       request = super().request(**kwargs)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.............../site-packages/rest_framework/test.py:225: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>
request = {'CONTENT_LENGTH': '1', 'CONTENT_TYPE': 'application/json', 'PATH_INFO': '.../flows/executor/u6t52ff6i1/', 'QUERY_STRING': '', ...}
environ = {'CONTENT_LENGTH': '1', 'CONTENT_TYPE': 'application/json', 'HTTP_COOKIE': '', 'PATH_INFO': '.../flows/executor/u6t52ff6i1/', ...}
data = {}
on_template_render = functools.partial(<function store_rendered_templates at 0x7fa581d18f60>, {})
signal_uid = 'template-render-140348514126528'
exception_uid = 'request-exception-140348514126528'
response = <HttpResponseNotAllowed [GET, HEAD, OPTIONS] status_code=405, "text/html; charset=utf-8">

    def request(self, **request):
        """
        Make a generic request. Compose the environment dictionary and pass
        to the handler, return the result of the handler. Assume defaults for
        the query environment, which can be overridden using the arguments to
        the request.
        """
        environ = self._base_environ(**request)
    
        # Curry a data dictionary into an instance of the template renderer
        # callback function.
        data = {}
        on_template_render = partial(store_rendered_templates, data)
        signal_uid = "template-render-%s" % id(request)
        signals.template_rendered.connect(on_template_render, dispatch_uid=signal_uid)
        # Capture exceptions created by the handler.
        exception_uid = "request-exception-%s" % id(request)
        got_request_exception.connect(self.store_exc_info, dispatch_uid=exception_uid)
        try:
            response = self.handler(environ)
        finally:
            signals.template_rendered.disconnect(dispatch_uid=signal_uid)
            got_request_exception.disconnect(dispatch_uid=exception_uid)
        # Check for signaled exceptions.
>       self.check_exception(response)

.venv/lib/python3.14.../django/test/client.py:1090: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rest_framework.test.APIClient object at 0x7fa570e87cb0>
response = <HttpResponseNotAllowed [GET, HEAD, OPTIONS] status_code=405, "text/html; charset=utf-8">

    def check_exception(self, response):
        """
        Look for a signaled exception, clear the current context exception
        data, re-raise the signaled exception, and clear the signaled exception
        from the local cache.
        """
        response.exc_info = self.exc_info
        if self.exc_info:
            _, exc_value, _ = self.exc_info
            self.exc_info = None
            if self.raise_request_exception:
>               raise exc_value

.venv/lib/python3.14.../django/test/client.py:805: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

request = <WSGIRequest: POST '.../flows/executor/u6t52ff6i1/'>

    @wraps(get_response)
    def inner(request):
        try:
>           response = get_response(request)
                       ^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../core/handlers/exception.py:55: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware object at 0x7fa570e857f0>
args = (<WSGIRequest: POST '.../flows/executor/u6t52ff6i1/'>,), kwargs = {}
f = <bound method AuditMiddleware.__call__ of <authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware object at 0x7fa5706d5590>>
middleware_span = None

    def __call__(self, *args: "Any", **kwargs: "Any") -> "Any":
        if hasattr(self, "async_route_check") and self.async_route_check():
            return self.__acall__(*args, **kwargs)
    
        f = self._call_method
        if f is None:
            self._call_method = f = self._inner.__call__
    
        middleware_span = _check_middleware_span(old_method=f)
    
        if middleware_span is None:
>           return f(*args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../integrations/django/middleware.py:169: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware object at 0x7fa5706d5590>
request = <WSGIRequest: POST '.../flows/executor/u6t52ff6i1/'>

    def __call__(self, request: HttpRequest) -> HttpResponse:
        _CTX_REQUEST.set(request)
>       self.connect(request)

authentik/events/middleware.py:157: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware object at 0x7fa5706d5590>
request = <WSGIRequest: POST '.../flows/executor/u6t52ff6i1/'>

    def connect(self, request: HttpRequest):
        super().connect(request)
>       if not self.enabled:
               ^^^^^^^^^^^^

.../enterprise/audit/middleware.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware object at 0x7fa5706d5590>

    @property
    def enabled(self):
        """Check if audit logging is enabled"""
>       return apps.get_app_config("authentik_enterprise").enabled()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../enterprise/audit/middleware.py:26: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <AuthentikEnterpriseConfig: authentik_enterprise>

    def enabled(self):
        """Return true if enterprise is enabled and valid"""
>       return self.check_enabled() or settings.TEST
               ^^^^^^^^^^^^^^^^^^^^

authentik/enterprise/apps.py:34: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <AuthentikEnterpriseConfig: authentik_enterprise>

    def check_enabled(self):
        """Actual enterprise check, cached"""
        from authentik.enterprise.license import LicenseKey
    
>       return LicenseKey.cached_summary().status.is_valid
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^

authentik/enterprise/apps.py:40: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    @staticmethod
    def cached_summary() -> LicenseSummary:
        """Helper method which looks up the last summary"""
        summary = cache.get(CACHE_KEY_ENTERPRISE_LICENSE)
        if not summary:
>           return LicenseKey.get_total().summary()
                   ^^^^^^^^^^^^^^^^^^^^^^

authentik/enterprise/license.py:244: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    @staticmethod
    def get_total() -> LicenseKey:
        """Get a summarized version of all (not expired) licenses"""
>       total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
                           ^^^^^^^^^^^^^^^^^

authentik/enterprise/license.py:143: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def get_license_aud() -> str:
        """Get the JWT audience field"""
>       return f"enterprise.goauthentik.io/license/{get_unique_identifier()}"
                                                    ^^^^^^^^^^^^^^^^^^^^^^^

authentik/enterprise/license.py:53: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def get_unique_identifier() -> str:
        """Get a globally unique identifier that does not change"""
>       install_id = get_install_id()
                     ^^^^^^^^^^^^^^^^

authentik/tenants/utils.py:20: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    @lru_cache
    def get_install_id() -> str:
        """Get install ID of this instance. The method is cached as the install ID is
        not expected to change"""
        from django.conf import settings
        from django.db import connection
    
        if settings.TEST:
            return str(uuid4())
        with connection.cursor() as cursor:
>           cursor.execute(QUERY)

authentik/root/install_id.py:28: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>, 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;')
kwargs = {}

    def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
        if sentry_sdk.get_client().get_integration(integration) is None:
            return original_function(*args, **kwargs)
    
>       return sentry_patched_function(*args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../site-packages/sentry_sdk/utils.py:1894: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>
sql = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None

    @ensure_integration_enabled(DjangoIntegration, real_execute)
    def execute(
        self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None
    ) -> "Any":
        with record_sql_queries(
            cursor=self.cursor,
            query=sql,
            params_list=params,
            paramstyle="format",
            executemany=False,
            span_origin=DjangoIntegration.origin_db,
        ) as span:
            _set_db_data(span, self)
>           result = real_execute(self, sql, params)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../integrations/django/__init__.py:645: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>
sql = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None

    def execute(self, sql, params=None):
>       return self._execute_with_wrappers(
            sql, params, many=False, executor=self._execute
        )

.venv/lib/python3.14.../db/backends/utils.py:79: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>
sql = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None, many = False
executor = <bound method CursorWrapper._execute of <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>>

    def _execute_with_wrappers(self, sql, params, many, executor):
        context = {"connection": self.db, "cursor": self}
        for wrapper in reversed(self.db.execute_wrappers):
            executor = functools.partial(wrapper, executor)
>       return executor(sql, params, many, context)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../db/backends/utils.py:92: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>
sql = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None
ignored_wrapper_args = (False, {'connection': <DatabaseWrapper vendor='postgresql' alias='default'>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        # Raise a warning during app initialization (stored_app_configs is only
        # ever set during testing).
        if not apps.ready and not apps.stored_app_configs:
            warnings.warn(self.APPS_NOT_READY_WARNING_MSG, category=RuntimeWarning)
        self.db.validate_no_broken_transaction()
>       with self.db.wrap_database_errors:
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../db/backends/utils.py:100: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.utils.DatabaseErrorWrapper object at 0x7fa57c6bfcb0>
exc_type = <class 'psycopg.errors.UndefinedTable'>
exc_value = UndefinedTable('relation "public.authentik_install_id" does not exist\nLINE 1: SELECT id FROM public.authentik_install_id ORDER BY id LIMIT...\n                       ^')
traceback = <traceback object at 0x7fa56f65a8c0>

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            return
        for dj_exc_type in (
            DataError,
            OperationalError,
            IntegrityError,
            InternalError,
            ProgrammingError,
            NotSupportedError,
            DatabaseError,
            InterfaceError,
            Error,
        ):
            db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__)
            if issubclass(exc_type, db_exc_type):
                dj_exc_value = dj_exc_type(*exc_value.args)
                # Only set the 'errors_occurred' flag for errors that may make
                # the connection unusable.
                if dj_exc_type not in (DataError, IntegrityError):
                    self.wrapper.errors_occurred = True
>               raise dj_exc_value.with_traceback(traceback) from exc_value

.venv/lib/python3.14.../django/db/utils.py:91: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>
sql = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None
ignored_wrapper_args = (False, {'connection': <DatabaseWrapper vendor='postgresql' alias='default'>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fa56fdabdd0>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        # Raise a warning during app initialization (stored_app_configs is only
        # ever set during testing).
        if not apps.ready and not apps.stored_app_configs:
            warnings.warn(self.APPS_NOT_READY_WARNING_MSG, category=RuntimeWarning)
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                # params default might be backend specific.
>               return self.cursor.execute(sql)
                       ^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../db/backends/utils.py:103: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django_prometheus.db.common.ExportingCursorWrapper.<locals>.CursorWrapper [closed] [INTRANS] (host=localhost user=authentik database=test_authentik) at 0x7fa571056eb0>
args = ('SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;',)
kwargs = {}

    def execute(self, *args, **kwargs):
        execute_total.labels(alias, vendor).inc()
        with (
            query_duration_seconds.labels(**labels).time(),
            ExceptionCounterByType(errors_total, extra_labels=labels),
        ):
>           return super().execute(*args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.venv/lib/python3.14.../django_prometheus/db/common.py:69: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django_prometheus.db.common.ExportingCursorWrapper.<locals>.CursorWrapper [closed] [INTRANS] (host=localhost user=authentik database=test_authentik) at 0x7fa571056eb0>
query = 'SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;'
params = None

    def execute(
        self,
        query: Query,
        params: Params | None = None,
        *,
        prepare: bool | None = None,
        binary: bool | None = None,
    ) -> Self:
        """
        Execute a query or command to the database.
        """
        try:
            with self._conn.lock:
                self._conn.wait(
                    self._execute_gen(query, params, prepare=prepare, binary=binary)
                )
        except e._NO_TRACEBACK as ex:
>           raise ex.with_traceback(None)
E           django.db.utils.ProgrammingError: relation "public.authentik_install_id" does not exist
E           LINE 1: SELECT id FROM public.authentik_install_id ORDER BY id LIMIT...
E                                  ^

.venv/lib/python3.14....../site-packages/psycopg/cursor.py:117: ProgrammingError
tests.e2e.test_endpoints_flow.TestEndpointsFlow::test_login
Stack Traces | 111s run time
self = <unittest.case._Outcome object at 0x7f9df1361fd0>
test_case = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>
subTest = False

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

.../hostedtoolcache/Python/3.14.5.............../x64/lib/python3.14/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>
result = <TestCaseFunction test_login>

    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.14.5.............../x64/lib/python3.14/unittest/case.py:669: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>
method = <bound method TestEndpointsFlow.test_login of <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>>

    def _callTestMethod(self, method):
>       result = method()
                 ^^^^^^^^

.../hostedtoolcache/Python/3.14.5.............../x64/lib/python3.14/unittest/case.py:615: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>
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/decorators.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>,)
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: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    def test_login(self):
        """test default login flow"""
        rc, output = self.driver_container.exec_run(
            ["ak-sysd", "domains", "join", "ak", "-a", self.live_server_url],
            user="root",
            environment={"AK_SYS_INSECURE_ENV_TOKEN": self.enrollment_token.key},
        )
>       self.assertEqual(rc, 0, str(output))

tests/e2e/test_endpoints_flow.py:41: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>
first = 1, second = 0
msg = 'b\'time="2026-05-27T11:03:01Z" level=info msg="finished call" grpc.code=Unknown grpc.component=client grpc.error="rpc...join\\n\\nGlobal Flags:\\n      --config-file string   Config file path (default "........./etc/authentik/config.json")\\n\\n\''

    def assertEqual(self, first, second, msg=None):
        """Fail if the two objects are unequal as determined by the '=='
           operator.
        """
        assertion_func = self._getAssertEqualityFunc(first, second)
>       assertion_func(first, second, msg=msg)

.../hostedtoolcache/Python/3.14.5.............../x64/lib/python3.14/unittest/case.py:925: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_endpoints_flow.TestEndpointsFlow testMethod=test_login>
first = 1, second = 0
msg = '1 != 0 : b\'time="2026-05-27T11:03:01Z" level=info msg="finished call" grpc.code=Unknown grpc.component=client grpc.e...join\\n\\nGlobal Flags:\\n      --config-file string   Config file path (default "........./etc/authentik/config.json")\\n\\n\''

    def _baseAssertEqual(self, first, second, msg=None):
        """The default assertEqual implementation, not type specific."""
        if not first == second:
            standardMsg = '%s != %s' % _common_shorten_repr(first, second)
            msg = self._formatMessage(msg, standardMsg)
>           raise self.failureException(msg)
E           AssertionError: 1 != 0 : b'time="2026-05-27T11:03:01Z" level=info msg="finished call" grpc.code=Unknown grpc.component=client grpc.error="rpc error: code = Unknown desc = failed to checkin: HTTP Error \'400 Bad Request\' during request \'POST .../agents/connectors/check_in/\': \\"{\\"os\\":{\\"version\\":[\\"This field may not be blank.\\"]}}\\" (Request ID \'dfaa075774294e0daa5ac450f182cfdd\')" grpc.method=DomainEnroll grpc.method_type=unary grpc.service=sys_ctrl.SystemCtrl grpc.start_time="2026-05-27T11:02:23Z" grpc.time_ms=38465.484 logger=cli.system_grpc protocol=grpc\nError: rpc error: code = Unknown desc = failed to checkin: HTTP Error \'400 Bad Request\' during request \'POST .../agents/connectors/check_in/\': "{"os":{"version":["This field may not be blank."]}}" (Request ID \'dfaa075774294e0daa5ac450f182cfdd\')\nUsage:\n  ak-sysd domains join [domain_name] [flags]\n\nFlags:\n  -a, --authentik-url string   URL to the authentik Instance\n  -h, --help                   help for join\n\nGlobal Flags:\n      --config-file string   Config file path (default "........./etc/authentik/config.json")\n\n'

.../hostedtoolcache/Python/3.14.5.............../x64/lib/python3.14/unittest/case.py:918: 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.

@BeryJu BeryJu merged commit 7dd26c2 into main May 27, 2026
205 of 219 checks passed
@BeryJu BeryJu deleted the providers/oauth2/fix-session-code branch May 27, 2026 20:35
@authentik-automation
Copy link
Copy Markdown
Contributor

🍒 Cherry-pick to version-2026.5 created: #22692

BeryJu added a commit that referenced this pull request May 27, 2026
…ry-pick #22684 to version-2026.5) (#22692)

providers/oauth2: fix session decode when upgrading from 2026.2 (#22684)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/version-2026.5 Add this label to PRs to backport changes to version-2026.5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GrantTypes to GrantType rename poisons all pre-upgrade sessions, leading to 404 on /application/o/authorize/

2 participants