quarkusio/quarkus · error · RuntimeException

Tenant id must have been set by either the session or state

Error message

Tenant id must have been set by either the session or state cookie

What it means

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.

Source

Thrown at integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java:25

import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        String path = context.normalizedPath();
        // `/hr-classic-perm-check` and '/hr-classic-and-jaxrs-perm-check'
        // require policy checks which force an authentication before @Tenant is resolved
        if (path.contains("/hr") && !path.contains("/hr-classic-perm-check")
                && !path.contains("/hr-classic-and-jaxrs-perm-check")) {
            throw new RuntimeException("@Tenant annotation only must be used to set "
                    + "a tenant id on the '" + path + "' request path");
        }
        if (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null) {
            if (context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null
                    && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) {
                throw new RuntimeException("Tenant id must have been set by either the session or state cookie");
            }
            // Expect an already resolved tenant context be used
            return null;
        }
        if (path.contains("recovered-no-discovery")) {
            return "no-discovery";
        }
        if (path.endsWith("code-flow") || path.endsWith("code-flow/logout")) {
            return "code-flow";
        }
        if (path.endsWith("code-flow-form-post") || path.endsWith("code-flow-form-post/front-channel-logout")) {
            return "code-flow-form-post";
        }

        return null;
    }
}

View on GitHub (pinned to e1c734241f)

Solutions

  1. Ensure the client sends the OIDC session and/or state cookies (enable cookie handling in the HTTP client).
  2. Check that no custom code sets OidcUtils.TENANT_ID_ATTRIBUTE without going through the OIDC session/state cookie path.
  3. Verify quarkus.oidc.<tenant> cookie/tenant configuration matches the tenant resolver expectations.
  4. Enable DEBUG logging for io.quarkus.oidc to see which mechanism resolved the tenant id.

Example fix

// before
if (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null) {
    if (context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null
            && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) {
        throw new RuntimeException("Tenant id must have been set by either the session or state cookie");
    }
    return null;
}
// after (client side: keep cookies across the code-flow)
// CookieHandler cookieHandler = new CookieManager();
// HttpClient client = HttpClient.newBuilder().cookieHandler(cookieHandler).build();
Defensive patterns

Strategy: validation

Validate before calling

if (context.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null
        && context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) == null
        && context.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) == null) {
    throw new IllegalStateException("tenant id present but no session/state cookie backing it");
}

Type guard

boolean hasCookieBackedTenant(RoutingContext ctx) {
    return ctx.get(OidcUtils.TENANT_ID_ATTRIBUTE) != null
            && (ctx.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE) != null
                || ctx.get(OidcUtils.TENANT_ID_SET_BY_STATE_COOKIE) != null);
}

Try / catch

try {
    return tenantResolver.resolve(context, tenantResolverFunction);
} catch (RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().contains("session or state cookie")) {
        // fall back to default tenant resolution
    }
    throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/2c8456fce3b84d63. Report an issue: GitHub.