All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.oidc.runtime.DefaultTenantConfigResolver Maven / Gradle / Ivy

Go to download

Secure your applications with OpenID Connect Adapter and IDP such as Keycloak

There is a newer version: 3.17.5
Show newest version
package io.quarkus.oidc.runtime;

import static io.quarkus.oidc.runtime.OidcProvider.ANY_ISSUER;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.jwt.Claims;
import org.jboss.logging.Logger;

import io.quarkus.oidc.JavaScriptRequestChecker;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.TenantResolver;
import io.quarkus.oidc.TokenIntrospectionCache;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.UserInfoCache;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class DefaultTenantConfigResolver {

    private static final Logger LOG = Logger.getLogger(DefaultTenantConfigResolver.class);
    private static final String CURRENT_STATIC_TENANT_ID = "static.tenant.id";
    private static final String CURRENT_STATIC_TENANT_ID_NULL = "static.tenant.id.null";
    private static final String CURRENT_DYNAMIC_TENANT_CONFIG = "dynamic.tenant.config";
    private final ConcurrentHashMap backChannelLogoutTokens = new ConcurrentHashMap<>();
    private final BlockingTaskRunner blockingRequestContext;
    private final boolean securityEventObserved;
    private final TenantConfigBean tenantConfigBean;
    private final TenantResolver[] staticTenantResolvers;
    private final boolean annotationBasedTenantResolutionEnabled;

    @Inject
    Instance tenantConfigResolver;

    @Inject
    Instance javaScriptRequestChecker;

    @Inject
    Instance tokenStateManager;

    @Inject
    Instance tokenIntrospectionCache;

    @Inject
    Instance userInfoCache;

    @Inject
    Event securityEvent;

    @Inject
    @ConfigProperty(name = "quarkus.http.proxy.enable-forwarded-prefix")
    boolean enableHttpForwardedPrefix;

    DefaultTenantConfigResolver(BlockingSecurityExecutor blockingExecutor, BeanManager beanManager,
            Instance 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(blockingExecutor);
        this.securityEventObserved = SecurityEventHelper.isEventObserved(new SecurityEvent(null, (SecurityIdentity) null),
                beanManager, securityEventsEnabled);
        this.tenantConfigBean = tenantConfigBean;
        this.staticTenantResolvers = prepareStaticTenantResolvers(tenantConfigBean, rootPath, tenantResolverInstance,
                resolveTenantsWithIssuer, new DefaultStaticTenantResolver());
        this.annotationBasedTenantResolutionEnabled = Boolean.getBoolean(OidcUtils.ANNOTATION_BASED_TENANT_RESOLUTION_ENABLED);
    }

    @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");
        }
    }

    Uni resolveConfig(RoutingContext context) {
        return getDynamicTenantConfig(context)
                .map(new Function() {
                    @Override
                    public OidcTenantConfig apply(OidcTenantConfig tenantConfig) {
                        if (tenantConfig == null) {

                            final String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);

                            if (tenantId != null && !isTenantSetByAnnotation(context, tenantId)) {
                                TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenantsConfig().get(tenantId);
                                if (tenantContext != null) {
                                    // Dynamic map may contain the static contexts initialized on demand,
                                    if (tenantConfigBean.getStaticTenantsConfig().containsKey(tenantId)) {
                                        context.put(CURRENT_STATIC_TENANT_ID, tenantId);
                                    }
                                    return tenantContext.getOidcTenantConfig();
                                }
                            }

                            TenantConfigContext tenant = getStaticTenantContext(context);
                            if (tenant != null) {
                                tenantConfig = tenant.oidcConfig;
                            }
                        }
                        return tenantConfig;
                    }
                });
    }

    Uni resolveContext(String tenantId) {
        return initializeStaticTenantIfContextNotReady(getStaticTenantContext(tenantId));
    }

    Uni resolveContext(RoutingContext context) {
        return getDynamicTenantContext(context).onItem().ifNull().switchTo(new Supplier>() {
            @Override
            public Uni get() {
                return initializeStaticTenantIfContextNotReady(getStaticTenantContext(context));
            }
        });
    }

    private Uni initializeStaticTenantIfContextNotReady(TenantConfigContext tenantContext) {
        if (tenantContext != null && !tenantContext.ready) {

            // check if the connection has already been created
            TenantConfigContext readyTenantContext = tenantConfigBean.getDynamicTenantsConfig()
                    .get(tenantContext.oidcConfig.tenantId.get());
            if (readyTenantContext == null) {
                LOG.debugf("Tenant '%s' is not initialized yet, trying to create OIDC connection now",
                        tenantContext.oidcConfig.tenantId.get());
                return tenantConfigBean.getTenantConfigContextFactory().apply(tenantContext.oidcConfig);
            } else {
                tenantContext = readyTenantContext;
            }
        }

        return Uni.createFrom().item(tenantContext);
    }

    private TenantConfigContext getStaticTenantContext(RoutingContext context) {
        String tenantId = context.get(CURRENT_STATIC_TENANT_ID);
        if (tenantId == null && context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) {
            tenantId = resolveStaticTenantId(context);
            if (tenantId != null) {
                context.put(CURRENT_STATIC_TENANT_ID, tenantId);
            } else {
                context.put(CURRENT_STATIC_TENANT_ID_NULL, true);
            }
        }

        return getStaticTenantContext(tenantId);
    }

    private String resolveStaticTenantId(RoutingContext context) {
        String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
        if (isTenantSetByAnnotation(context, tenantId)) {
            return tenantId;
        }

        for (var staticTenantResolver : staticTenantResolvers) {
            tenantId = staticTenantResolver.resolve(context);
            if (tenantId != null) {
                return tenantId;
            }
        }

        return context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
    }

    private boolean isTenantSetByAnnotation(RoutingContext context, String tenantId) {
        return annotationBasedTenantResolutionEnabled &&
                tenantId != null && context.get(OidcUtils.TENANT_ID_SET_BY_ANNOTATION) != null;
    }

    private TenantConfigContext getStaticTenantContext(String tenantId) {
        TenantConfigContext configContext = tenantId != null ? tenantConfigBean.getStaticTenantsConfig().get(tenantId) : null;
        if (configContext == null) {
            if (tenantId != null && !tenantId.isEmpty()) {
                LOG.debugf(
                        "Registered TenantResolver has not provided the configuration for tenant '%s', using the default tenant",
                        tenantId);
            }
            configContext = tenantConfigBean.getDefaultTenant();
        }
        return configContext;
    }

    boolean isSecurityEventObserved() {
        return securityEventObserved;
    }

    Event getSecurityEvent() {
        return securityEvent;
    }

    TokenStateManager getTokenStateManager() {
        return tokenStateManager.get();
    }

    TokenIntrospectionCache getTokenIntrospectionCache() {
        return tokenIntrospectionCache.isResolvable() ? tokenIntrospectionCache.get() : null;
    }

    UserInfoCache getUserInfoCache() {
        return userInfoCache.isResolvable() ? userInfoCache.get() : null;
    }

    private Uni getDynamicTenantConfig(RoutingContext context) {
        if (isTenantSetByAnnotation(context, context.get(OidcUtils.TENANT_ID_ATTRIBUTE))) {
            return Uni.createFrom().nullItem();
        }
        if (tenantConfigResolver.isResolvable()) {
            Uni oidcConfig = context.get(CURRENT_DYNAMIC_TENANT_CONFIG);
            if (oidcConfig == null) {
                oidcConfig = tenantConfigResolver.get().resolve(context, blockingRequestContext);
                if (oidcConfig == null) {
                    //shouldn't happen, but guard against it anyway
                    oidcConfig = Uni.createFrom().nullItem();
                }
                oidcConfig = oidcConfig.memoize().indefinitely();
                if (oidcConfig == null) {
                    //shouldn't happen, but guard against it anyway
                    oidcConfig = Uni.createFrom().nullItem();
                } else {
                    oidcConfig = oidcConfig.onItem().transform(cfg -> OidcUtils.resolveProviderConfig(cfg));
                }
                context.put(CURRENT_DYNAMIC_TENANT_CONFIG, oidcConfig);
            }
            return oidcConfig;
        }
        return Uni.createFrom().nullItem();
    }

    private Uni getDynamicTenantContext(RoutingContext context) {

        return getDynamicTenantConfig(context).chain(new Function>() {
            @Override
            public Uni apply(OidcTenantConfig tenantConfig) {
                if (tenantConfig != null) {
                    String tenantId = tenantConfig.getTenantId()
                            .orElseThrow(() -> new OIDCException("Tenant configuration must have tenant id"));
                    TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenantsConfig().get(tenantId);
                    if (tenantContext == null) {
                        return tenantConfigBean.getTenantConfigContextFactory().apply(tenantConfig);
                    } else {
                        return Uni.createFrom().item(tenantContext);
                    }
                } else {
                    final String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
                    if (tenantId != null && !isTenantSetByAnnotation(context, tenantId)) {
                        TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenantsConfig().get(tenantId);
                        if (tenantContext != null) {
                            return Uni.createFrom().item(tenantContext);
                        }
                    }
                }
                return Uni.createFrom().nullItem();
            }
        });
    }

    boolean isEnableHttpForwardedPrefix() {
        return enableHttpForwardedPrefix;
    }

    public Map getBackChannelLogoutTokens() {
        return backChannelLogoutTokens;
    }

    public TenantConfigBean getTenantConfigBean() {
        return tenantConfigBean;
    }

    public JavaScriptRequestChecker getJavaScriptRequestChecker() {
        return javaScriptRequestChecker.isResolvable() ? javaScriptRequestChecker.get() : null;
    }

    private static TenantResolver[] prepareStaticTenantResolvers(TenantConfigBean tenantConfigBean, String rootPath,
            Instance tenantResolverInstance, boolean resolveTenantsWithIssuer,
            TenantResolver defaultStaticTenantResolver) {
        List staticTenantResolvers = new ArrayList<>();
        // STATIC TENANT RESOLVERS BY PRIORITY:
        // 0. annotation based resolver

        // 1. custom tenant resolver
        if (tenantResolverInstance.isResolvable()) {
            if (tenantResolverInstance.isAmbiguous()) {
                throw new IllegalStateException("Multiple " + TenantResolver.class + " beans registered");
            }
            staticTenantResolvers.add(tenantResolverInstance.get());
        }

        // 2. path-matching tenant resolver
        var pathMatchingTenantResolver = PathMatchingTenantResolver.of(tenantConfigBean.getStaticTenantsConfig(), rootPath,
                tenantConfigBean.getDefaultTenant());
        if (pathMatchingTenantResolver != null) {
            staticTenantResolvers.add(pathMatchingTenantResolver);
        }

        // 3. default static tenant resolver
        if (!tenantConfigBean.getStaticTenantsConfig().isEmpty()) {
            staticTenantResolvers.add(defaultStaticTenantResolver);
        }

        // 4. issuer-based tenant resolver
        if (resolveTenantsWithIssuer) {
            IssuerBasedTenantResolver.addIssuerBasedTenantResolver(staticTenantResolvers,
                    tenantConfigBean.getStaticTenantsConfig(), tenantConfigBean.getDefaultTenant());
        }

        return staticTenantResolvers.toArray(new TenantResolver[0]);
    }

    private class DefaultStaticTenantResolver implements TenantResolver {

        @Override
        public String resolve(RoutingContext context) {
            String[] pathSegments = context.request().path().split("/");
            if (pathSegments.length > 0) {
                String lastPathSegment = pathSegments[pathSegments.length - 1];
                if (tenantConfigBean.getStaticTenantsConfig().containsKey(lastPathSegment)) {
                    LOG.debugf(
                            "Tenant id '%s' is selected on the '%s' request path", lastPathSegment, context.normalizedPath());
                    return lastPathSegment;
                }
            }
            return null;
        }
    }

    private static class PathMatchingTenantResolver implements TenantResolver {
        private static final String DEFAULT_TENANT = "PathMatchingTenantResolver#DefaultTenant";
        private final ImmutablePathMatcher staticTenantPaths;

        private PathMatchingTenantResolver(ImmutablePathMatcher staticTenantPaths) {
            this.staticTenantPaths = staticTenantPaths;
        }

        private static PathMatchingTenantResolver of(Map staticTenantsConfig, String rootPath,
                TenantConfigContext defaultTenant) {
            final var builder = ImmutablePathMatcher. builder().rootPath(rootPath);
            addPath(DEFAULT_TENANT, defaultTenant.oidcConfig, builder);
            for (Map.Entry e : staticTenantsConfig.entrySet()) {
                addPath(e.getKey(), e.getValue().oidcConfig, builder);
            }
            return builder.hasPaths() ? new PathMatchingTenantResolver(builder.build()) : null;
        }

        @Override
        public String resolve(RoutingContext context) {
            String tenantId = staticTenantPaths.match(context.normalizedPath()).getValue();
            if (tenantId != null) {
                LOG.debugf(
                        "Tenant id '%s' is selected on the '%s' request path", tenantId, context.normalizedPath());
                return tenantId;
            }
            return null;
        }

        private static ImmutablePathMatcher.ImmutablePathMatcherBuilder addPath(String tenant, OidcTenantConfig config,
                ImmutablePathMatcher.ImmutablePathMatcherBuilder builder) {
            if (config != null && config.tenantPaths.isPresent()) {
                for (String path : config.tenantPaths.get()) {
                    builder.addPath(path, tenant);
                }
            }
            return builder;
        }
    }

    public OidcTenantConfig getResolvedConfig(String sessionTenantId) {
        if (OidcUtils.DEFAULT_TENANT_ID.equals(sessionTenantId)) {
            return tenantConfigBean.getDefaultTenant().getOidcTenantConfig();
        }

        if (tenantConfigBean.getStaticTenantsConfig().containsKey(sessionTenantId)) {
            return tenantConfigBean.getStaticTenantsConfig().get(sessionTenantId).getOidcTenantConfig();
        }

        if (tenantConfigBean.getDynamicTenantsConfig().containsKey(sessionTenantId)) {
            return tenantConfigBean.getDynamicTenantsConfig().get(sessionTenantId).getOidcTenantConfig();
        }
        return null;
    }

    private static final class IssuerBasedTenantResolver implements TenantResolver {

        private final TenantConfigContext[] tenantConfigContexts;
        private final boolean detectedTenantWithoutMetadata;

        private IssuerBasedTenantResolver(TenantConfigContext[] tenantConfigContexts, boolean detectedTenantWithoutMetadata) {
            this.tenantConfigContexts = tenantConfigContexts;
            this.detectedTenantWithoutMetadata = detectedTenantWithoutMetadata;
        }

        @Override
        public String resolve(RoutingContext context) {
            for (var tenantContext : tenantConfigContexts) {
                if (detectedTenantWithoutMetadata
                        && (tenantContext.getOidcMetadata() == null || tenantContext.getOidcMetadata().getIssuer() == null
                                || ANY_ISSUER.equals(tenantContext.getOidcMetadata().getIssuer()))) {
                    // this is static tenant that didn't have OIDC metadata available at startup
                    continue;
                }

                final String token = OidcUtils.extractBearerToken(context, tenantContext.oidcConfig);
                if (token != null && !OidcUtils.isOpaqueToken(token)) {
                    final var tokenJson = OidcUtils.decodeJwtContent(token);
                    if (tokenJson != null) {

                        final String iss = tokenJson.getString(Claims.iss.name());
                        if (tenantContext.getOidcMetadata().getIssuer().equals(iss)) {
                            OidcUtils.storeExtractedBearerToken(context, token);

                            final String tenantId = tenantContext.oidcConfig.tenantId.get();
                            LOG.debugf("Resolved the '%s' OIDC tenant based on the matching issuer '%s'", tenantId, iss);
                            return tenantId;
                        }
                    }
                }
            }
            return null;
        }

        private static TenantResolver of(Map tenantConfigContexts) {
            var contextsWithIssuer = new ArrayList();
            boolean detectedTenantWithoutMetadata = false;
            for (TenantConfigContext context : tenantConfigContexts.values()) {
                if (context.oidcConfig.tenantEnabled && !OidcUtils.isWebApp(context.oidcConfig)) {
                    if (context.getOidcMetadata() == null) {
                        // if the tenant metadata are not available, we can't decide now
                        detectedTenantWithoutMetadata = true;
                        contextsWithIssuer.add(context);
                    } else if (context.getOidcMetadata().getIssuer() != null
                            && !ANY_ISSUER.equals(context.getOidcMetadata().getIssuer())) {
                        contextsWithIssuer.add(context);
                    }
                }
            }
            if (contextsWithIssuer.isEmpty()) {
                return null;
            } else {
                return new IssuerBasedTenantResolver(contextsWithIssuer.toArray(new TenantConfigContext[0]),
                        detectedTenantWithoutMetadata);
            }
        }

        private static void addIssuerBasedTenantResolver(List resolvers,
                Map staticTenantsConfig, TenantConfigContext defaultTenant) {
            Map tenantConfigContexts = new HashMap<>(staticTenantsConfig);
            tenantConfigContexts.put(OidcUtils.DEFAULT_TENANT_ID, defaultTenant);
            var issuerTenantResolver = IssuerBasedTenantResolver.of(tenantConfigContexts);
            if (issuerTenantResolver != null) {
                resolvers.add(issuerTenantResolver);
            } else {
                LOG.debug("The 'quarkus.oidc.resolve-tenants-with-issuer' configuration property is set to true, "
                        + "but no static tenant supports this feature. To use this feature, please configure at least "
                        + "one static tenant with the discovered or configured issuer and set either 'service' or "
                        + "'hybrid' application type");
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy