{"record":{"id":"2c8456fce3b84d63","repo":"quarkusio/quarkus","slug":"tenant-id-must-have-been-set-by-either-the-session","errorCode":null,"errorMessage":"Tenant id must have been set by either the session or state cookie","messagePattern":"Tenant id must have been set by either the session or state cookie","errorType":"exception","errorClass":"RuntimeException","httpStatus":500,"severity":"error","filePath":"integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java","lineNumber":25,"sourceCode":"import io.vertx.ext.web.RoutingContext;\n\n@ApplicationScoped\npublic class CustomTenantResolver implements TenantResolver {\n\n    @Override\n    public String resolve(RoutingContext context) {\n        String path = context.normalizedPath();\n        // `/hr-classic-perm-check` and '/hr-classic-and-jaxrs-perm-check'\n        // require policy checks which force an authentication before @Tenant is resolved\n        if (path.contains(\"/hr\") && !path.contains(\"/hr-classic-perm-check\")\n                && !path.contains(\"/hr-classic-and-jaxrs-perm-check\")) {\n            throw new RuntimeException(\"@Tenant annotation only must be used to set \"\n                    + \"a tenant id on the '\" + path + \"' request path\");\n        }\n        if (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null) {\n            if (context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null\n                    && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) {\n                throw new RuntimeException(\"Tenant id must have been set by either the session or state cookie\");\n            }\n            // Expect an already resolved tenant context be used\n            return null;\n        }\n        if (path.contains(\"recovered-no-discovery\")) {\n            return \"no-discovery\";\n        }\n        if (path.endsWith(\"code-flow\") || path.endsWith(\"code-flow/logout\")) {\n            return \"code-flow\";\n        }\n        if (path.endsWith(\"code-flow-form-post\") || path.endsWith(\"code-flow-form-post/front-channel-logout\")) {\n            return \"code-flow-form-post\";\n        }\n\n        return null;\n    }\n}\n","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java#L7-L43","documentation":"This RuntimeException is thrown by the CustomTenantResolver when the request context already carries a resolved TENANT_ID_ATTRIBUTE but no session or state cookie is present that could have set it. Quarkus OIDC expects an already-resolved tenant id to be traceable to either the OIDC session cookie or the authorization-request state cookie; otherwise the tenant selection is inconsistent. The resolver returns null to signal 'reuse existing tenant context' only in that valid case.","triggerScenarios":"A request hits a path whose tenant id was already resolved and stored in the routing context under OidcUtils.TENANT_ID_ATTRIBUTE, but neither TENANT_ID_SET_BY_SESSION_COOKIE nor TENANT_ID_SET_BY_STATE_COOKIE context attributes are set — e.g. a tenant was injected by another mechanism rather than via OIDC cookies.","commonSituations":"Misconfigured tenant resolution order, custom filters setting the tenant attribute manually, changes to OIDC session/state cookie naming, or requests replayed without cookies (cookie-less client) hitting tenant-protected endpoints.","solutions":["Ensure the client sends the OIDC session and/or state cookies (enable cookie handling in the HTTP client).","Check that no custom code sets OidcUtils.TENANT_ID_ATTRIBUTE without going through the OIDC session/state cookie path.","Verify quarkus.oidc.<tenant> cookie/tenant configuration matches the tenant resolver expectations.","Enable DEBUG logging for io.quarkus.oidc to see which mechanism resolved the tenant id."],"exampleFix":"// before\nif (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null) {\n    if (context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null\n            && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) {\n        throw new RuntimeException(\"Tenant id must have been set by either the session or state cookie\");\n    }\n    return null;\n}\n// after (client side: keep cookies across the code-flow)\n// CookieHandler cookieHandler = new CookieManager();\n// HttpClient client = HttpClient.newBuilder().cookieHandler(cookieHandler).build();","handlingStrategy":"validation","validationCode":"if (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null\n        && context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null\n        && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) {\n    throw new IllegalStateException(\"tenant id present but no session/state cookie backing it\");\n}","typeGuard":"boolean hasCookieBackedTenant(RoutingContext ctx) {\n    return ctx.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null\n            && (ctx.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) != null\n                || ctx.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) != null);\n}","tryCatchPattern":"try {\n    return tenantResolver.resolve(context, tenantResolverFunction);\n} catch (RuntimeException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"session or state cookie\")) {\n        // fall back to default tenant resolution\n    }\n    throw e;\n}","preventionTips":["Always keep cookies (session + state) across the OIDC code-flow in test clients.","Never set OidcUtils.TENANT_ID_ATTRIBUTE from custom code outside the OIDC session/state cookie path.","Document tenant resolution order for custom multi-tenancy setups.","Enable io.quarkus.oidc DEBUG logging when changing tenant resolution logic."],"tags":["oidc","tenant","cookie","multi-tenancy"],"backgroundTag":"oidc-tenant-resolution","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}