quarkusio/quarkus · critical · IllegalStateException

Multiple interface io.quarkus.oidc.TenantConfigResolver bean

Error message

Multiple interface io.quarkus.oidc.TenantConfigResolver beans registered

What it means

DefaultTenantConfigResolver's @PostConstruct verifyResolvers() checks CDI injected Instances for ambiguity. If more than one bean implements io.quarkus.oidc.TenantConfigResolver and is resolvable, Quarkus cannot pick a single tenant resolver, so startup fails with IllegalStateException naming the interface.

Source

Thrown at extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java:98

    DefaultTenantConfigResolver(BlockingSecurityExecutor blockingExecutor, BeanManager beanManager,
            Instance<TenantResolver> tenantResolverInstance,
            @ConfigProperty(name = "quarkus.oidc.resolve-tenants-with-issuer") boolean resolveTenantsWithIssuer,
            @ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled,
            @ConfigProperty(name = "quarkus.http.root-path") String rootPath, TenantConfigBean tenantConfigBean) {
        this.blockingRequestContext = new BlockingTaskRunner<OidcTenantConfig>(blockingExecutor);
        this.securityEventObserved = SecurityEventHelper.isEventObserved(new SecurityEvent(null, (SecurityIdentity) null),
                beanManager, securityEventsEnabled);
        this.tenantConfigBean = tenantConfigBean;
        this.annotationBasedTenantResolutionEnabled = Boolean.getBoolean(OidcUtils.ANNOTATION_BASED_TENANT_RESOLUTION_ENABLED);
        this.rootPath = rootPath;
        this.staticTenantResolver = new StaticTenantResolver(tenantConfigBean, rootPath, resolveTenantsWithIssuer,
                tenantResolverInstance);
    }

    @PostConstruct
    public void verifyResolvers() {
        if (tenantConfigResolver.isResolvable() && tenantConfigResolver.isAmbiguous()) {
            throw new IllegalStateException("Multiple " + TenantConfigResolver.class + " beans registered");
        }
        if (tokenStateManager.isAmbiguous()) {
            throw new IllegalStateException("Multiple " + TokenStateManager.class + " beans registered");
        }
        if (tokenIntrospectionCache.isAmbiguous()) {
            throw new IllegalStateException("Multiple " + TokenIntrospectionCache.class + " beans registered");
        }
        if (userInfoCache.isAmbiguous()) {
            throw new IllegalStateException("Multiple " + UserInfo.class + " beans registered");
        }
        if (javaScriptRequestChecker.isAmbiguous()) {
            throw new IllegalStateException("Multiple " + JavaScriptRequestChecker.class + " beans registered");
        }

    }

    List<AuthenticationCompletionAction> authenticationCompletionActions() {
        return authenticationCompletionActions;

View on GitHub (pinned to e1c734241f)

Solutions

  1. Keep only one TenantConfigResolver bean; delete or merge the duplicate implementation.
  2. Mark one bean with @Alternative/@Priority or @Default (and the others with @Alternative) so only one is resolvable.
  3. If you intended to support multiple tenants, use static quarkus.oidc.<tenant>.* configuration or one resolver that dispatches by RoutingContext, not multiple resolvers.

Example fix

// before
class TenantAResolver implements TenantConfigResolver { ... }
class TenantBResolver implements TenantConfigResolver { ... }

// after - single resolver dispatching per request
class AppTenantResolver implements TenantConfigResolver {
    public Uni<OidcTenantConfig> resolve(RoutingContext ctx) {
        return ctx.request().host().startsWith("b.") ? tenantB() : tenantA();
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Before deploying: assert exactly one TenantConfigResolver bean
import jakarta.enterprise.inject.Instance;
Instance<TenantConfigResolver> resolvers = CDI.current().select(TenantConfigResolver.class);
if (resolvers.stream().count() > 1) {
    throw new IllegalStateException("Register only one TenantConfigResolver");
}

Prevention

When it happens

Trigger: Deploy an application with two or more @ApplicationScoped beans implementing TenantConfigResolver that are both eligible for injection into DefaultTenantConfigResolver (no @Default/@Alternative disambiguation), then start the app; the PostConstruct on DefaultTenantConfigResolver throws.

Common situations: Adding a second custom TenantConfigResolver for multi-tenant OIDC while another one (or a library-provided one) already exists; copying example code that defines a resolver while keeping an old one; version upgrades bringing in an extra resolver bean.

Related errors


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