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

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

package io.quarkus.oidc.runtime;

import static io.quarkus.oidc.SecurityEvent.AUTH_SERVER_URL;
import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_AVAILABLE;
import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_NOT_AVAILABLE;
import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute;

import java.security.Key;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.PublicJsonWebKey;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.runtime.OidcCommonConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.MethodDescription;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.mutiny.ext.web.client.WebClient;

@Recorder
public class OidcRecorder {

    private static final Logger LOG = Logger.getLogger(OidcRecorder.class);
    private static final String SECURITY_EVENTS_ENABLED_CONFIG_KEY = "quarkus.security.events.enabled";

    private static final Map dynamicTenantsConfig = new ConcurrentHashMap<>();
    private static final Set tenantsExpectingServerAvailableEvents = ConcurrentHashMap.newKeySet();

    public Supplier setupTokenCache(OidcConfig config, Supplier vertx) {
        return new Supplier() {
            @Override
            public DefaultTokenIntrospectionUserInfoCache get() {
                return new DefaultTokenIntrospectionUserInfoCache(config, vertx.get());
            }
        };
    }

    public Supplier setup(OidcConfig config, Supplier vertx, TlsConfig tlsConfig) {
        final Vertx vertxValue = vertx.get();

        String defaultTenantId = config.defaultTenant.getTenantId().orElse(DEFAULT_TENANT_ID);
        TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant,
                !config.namedTenants.isEmpty(), tlsConfig, defaultTenantId);

        Map staticTenantsConfig = new HashMap<>();
        for (Map.Entry tenant : config.namedTenants.entrySet()) {
            OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), tenant.getValue().getTenantId());
            staticTenantsConfig.put(tenant.getKey(),
                    createStaticTenantContext(vertxValue, tenant.getValue(), false, tlsConfig, tenant.getKey()));
        }

        return new Supplier() {
            @Override
            public TenantConfigBean get() {
                return new TenantConfigBean(staticTenantsConfig, dynamicTenantsConfig, defaultTenantContext,
                        new Function>() {
                            @Override
                            public Uni apply(OidcTenantConfig config) {
                                return createDynamicTenantContext(vertxValue, config, tlsConfig, config.getTenantId().get());
                            }
                        });
            }
        };
    }

    public RuntimeValue methodInfoToDescription(String className, String methodName, String[] paramTypes) {
        return new RuntimeValue<>(new MethodDescription(className, methodName, paramTypes));
    }

    private Uni createDynamicTenantContext(Vertx vertx,
            OidcTenantConfig oidcConfig, TlsConfig tlsConfig, String tenantId) {

        if (oidcConfig.logout.backchannel.path.isPresent()) {
            throw new ConfigurationException(
                    "BackChannel Logout is currently not supported for dynamic tenants");
        }
        if (!dynamicTenantsConfig.containsKey(tenantId)) {
            Uni uniContext = createTenantContext(vertx, oidcConfig, false, tlsConfig, tenantId);
            uniContext.onFailure().transform(new Function() {
                @Override
                public Throwable apply(Throwable t) {
                    return logTenantConfigContextFailure(t, tenantId);
                }
            });
            return uniContext.onItem().transform(
                    new Function() {
                        @Override
                        public TenantConfigContext apply(TenantConfigContext t) {
                            dynamicTenantsConfig.putIfAbsent(tenantId, t);
                            return t;
                        }
                    });
        } else {
            return Uni.createFrom().item(dynamicTenantsConfig.get(tenantId));
        }
    }

    private TenantConfigContext createStaticTenantContext(Vertx vertx,
            OidcTenantConfig oidcConfig, boolean checkNamedTenants, TlsConfig tlsConfig, String tenantId) {

        Uni uniContext = createTenantContext(vertx, oidcConfig, checkNamedTenants, tlsConfig, tenantId);
        return uniContext.onFailure()
                .recoverWithItem(new Function() {
                    @Override
                    public TenantConfigContext apply(Throwable t) {
                        if (t instanceof OIDCException) {
                            LOG.warnf("Tenant '%s': '%s'."
                                    + " OIDC server is not available yet, an attempt to connect will be made during the first request."
                                    + " Access to resources protected by this tenant may fail"
                                    + " if OIDC server will not become available",
                                    tenantId, t.getMessage());
                            return new TenantConfigContext(null, oidcConfig, false);
                        }
                        logTenantConfigContextFailure(t, tenantId);
                        if (t instanceof ConfigurationException
                                && !oidcConfig.authServerUrl.isPresent() && LaunchMode.DEVELOPMENT == LaunchMode.current()) {
                            // Let it start if it is a DEV mode and auth-server-url has not been configured yet
                            return new TenantConfigContext(null, oidcConfig, false);
                        }
                        // fail in all other cases
                        throw new OIDCException(t);
                    }
                })
                .await().atMost(oidcConfig.getConnectionTimeout());
    }

    private static Throwable logTenantConfigContextFailure(Throwable t, String tenantId) {
        LOG.debugf(
                "'%s' tenant is not initialized: '%s'. Access to resources protected by this tenant will fail.",
                tenantId, t.getMessage());
        return t;
    }

    @SuppressWarnings("resource")
    private Uni createTenantContext(Vertx vertx, OidcTenantConfig oidcTenantConfig,
            boolean checkNamedTenants,
            TlsConfig tlsConfig, String tenantId) {
        if (!oidcTenantConfig.tenantId.isPresent()) {
            oidcTenantConfig.tenantId = Optional.of(tenantId);
        }

        final OidcTenantConfig oidcConfig = OidcUtils.resolveProviderConfig(oidcTenantConfig);

        if (!oidcConfig.tenantEnabled) {
            LOG.debugf("'%s' tenant configuration is disabled", tenantId);
            return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig));
        }

        if (!oidcConfig.getAuthServerUrl().isPresent()) {
            if (oidcConfig.getPublicKey().isPresent() && oidcConfig.certificateChain.trustStoreFile.isPresent()) {
                throw new ConfigurationException("Both public key and certificate chain verification modes are enabled");
            }
            if (oidcConfig.getPublicKey().isPresent()) {
                return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig));
            }

            if (oidcConfig.certificateChain.trustStoreFile.isPresent()) {
                return Uni.createFrom().item(createTenantContextToVerifyCertChain(oidcConfig));
            }
        }

        try {
            if (!oidcConfig.getAuthServerUrl().isPresent()) {
                if (DEFAULT_TENANT_ID.equals(oidcConfig.tenantId.get())) {
                    ArcContainer container = Arc.container();
                    if (container != null
                            && (container.instance(TenantConfigResolver.class).isAvailable() || checkNamedTenants)) {
                        LOG.debugf("Default tenant is not configured and will be disabled"
                                + " because either 'TenantConfigResolver' which will resolve tenant configurations is registered"
                                + " or named tenants are configured.");
                        oidcConfig.setTenantEnabled(false);
                        return Uni.createFrom()
                                .item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig));
                    }
                }
                throw new ConfigurationException("'quarkus.oidc.auth-server-url' property must be configured");
            }
            OidcCommonUtils.verifyEndpointUrl(oidcConfig.getAuthServerUrl().get());
            OidcCommonUtils.verifyCommonConfiguration(oidcConfig, OidcUtils.isServiceApp(oidcConfig), true);
        } catch (ConfigurationException t) {
            return Uni.createFrom().failure(t);
        }

        if (oidcConfig.roles.source.orElse(null) == Source.userinfo && !enableUserInfo(oidcConfig)) {
            throw new ConfigurationException(
                    "UserInfo is not required but UserInfo is expected to be the source of authorization roles");
        }
        if (oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false) && !OidcUtils.isWebApp(oidcConfig)
                && !enableUserInfo(oidcConfig)) {
            throw new ConfigurationException(
                    "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled");
        }
        if (!oidcConfig.authentication.isIdTokenRequired().orElse(true) && !enableUserInfo(oidcConfig)) {
            throw new ConfigurationException(
                    "UserInfo is not required but it will be needed to verify a code flow access token");
        }

        if (!oidcConfig.discoveryEnabled.orElse(true)) {
            if (!OidcUtils.isServiceApp(oidcConfig)) {
                if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) {
                    throw new ConfigurationException(
                            "'web-app' applications must have 'authorization-path' and 'token-path' properties "
                                    + "set when the discovery is disabled.",
                            Set.of("quarkus.oidc.authorization-path", "quarkus.oidc.token-path"));
                }
            }
            // JWK and introspection endpoints have to be set for both 'web-app' and 'service' applications
            if (!oidcConfig.jwksPath.isPresent() && !oidcConfig.introspectionPath.isPresent()) {
                if (!oidcConfig.authentication.isIdTokenRequired().orElse(true)
                        && oidcConfig.authentication.isUserInfoRequired().orElse(false)) {
                    LOG.debugf("tenant %s supports only UserInfo", oidcConfig.tenantId.get());
                } else {
                    throw new ConfigurationException(
                            "Either 'jwks-path' or 'introspection-path' properties must be set when the discovery is disabled.",
                            Set.of("quarkus.oidc.jwks-path", "quarkus.oidc.introspection-path"));
                }
            }
            if (oidcConfig.authentication.userInfoRequired.orElse(false) && !oidcConfig.userInfoPath.isPresent()) {
                throw new ConfigurationException(
                        "UserInfo is required but 'quarkus.oidc.user-info-path' is not configured.",
                        Set.of("quarkus.oidc.user-info-path"));
            }
        }

        if (OidcUtils.isServiceApp(oidcConfig)) {
            if (oidcConfig.token.refreshExpired) {
                throw new ConfigurationException(
                        "The 'token.refresh-expired' property can only be enabled for " + ApplicationType.WEB_APP
                                + " application types");
            }
            if (!oidcConfig.token.refreshTokenTimeSkew.isEmpty()) {
                throw new ConfigurationException(
                        "The 'token.refresh-token-time-skew' property can only be enabled for " + ApplicationType.WEB_APP
                                + " application types");
            }
            if (oidcConfig.logout.path.isPresent()) {
                throw new ConfigurationException(
                        "The 'logout.path' property can only be enabled for " + ApplicationType.WEB_APP
                                + " application types");
            }
            if (oidcConfig.roles.source.isPresent() && oidcConfig.roles.source.get() == Source.idtoken) {
                throw new ConfigurationException(
                        "The 'roles.source' property can only be set to 'idtoken' for " + ApplicationType.WEB_APP
                                + " application types");
            }
        } else {
            if (!oidcConfig.token.refreshTokenTimeSkew.isEmpty()) {
                oidcConfig.token.setRefreshExpired(true);
            }
        }

        if (oidcConfig.tokenStateManager.strategy != Strategy.KEEP_ALL_TOKENS) {

            if (oidcConfig.authentication.isUserInfoRequired().orElse(false)
                    || oidcConfig.roles.source.orElse(null) == Source.userinfo) {
                throw new ConfigurationException(
                        "UserInfo is required but DefaultTokenStateManager is configured to not keep the access token");
            }
            if (oidcConfig.roles.source.orElse(null) == Source.accesstoken) {
                throw new ConfigurationException(
                        "Access token is required to check the roles but DefaultTokenStateManager is configured to not keep the access token");
            }
        }

        if (oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false)) {
            if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
                if (oidcConfig.userInfoPath.isEmpty()) {
                    throw new ConfigurationException(
                            "UserInfo path is missing but 'verifyAccessTokenWithUserInfo' is enabled");
                }
                if (oidcConfig.introspectionPath.isPresent()) {
                    throw new ConfigurationException(
                            "Introspection path is configured and 'verifyAccessTokenWithUserInfo' is enabled, these options are mutually exclusive");
                }
            }
        }

        return createOidcProvider(oidcConfig, tlsConfig, vertx)
                .onItem().transform(new Function() {
                    @Override
                    public TenantConfigContext apply(OidcProvider p) {
                        return new TenantConfigContext(p, oidcConfig);
                    }
                });
    }

    private static boolean enableUserInfo(OidcTenantConfig oidcConfig) {
        Optional userInfoRequired = oidcConfig.authentication.isUserInfoRequired();
        if (userInfoRequired.isPresent()) {
            if (!userInfoRequired.get()) {
                return false;
            }
        } else {
            oidcConfig.authentication.setUserInfoRequired(true);
        }
        return true;
    }

    private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantConfig oidcConfig) {
        if (!OidcUtils.isServiceApp(oidcConfig)) {
            throw new ConfigurationException("'public-key' property can only be used with the 'service' applications");
        }
        LOG.debug("'public-key' property for the local token verification is set,"
                + " no connection to the OIDC server will be created");

        return new TenantConfigContext(
                new OidcProvider(oidcConfig.publicKey.get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig);
    }

    private static TenantConfigContext createTenantContextToVerifyCertChain(OidcTenantConfig oidcConfig) {
        if (!OidcUtils.isServiceApp(oidcConfig)) {
            throw new ConfigurationException(
                    "Currently only 'service' applications can be used to verify tokens with inlined certificate chains");
        }

        return new TenantConfigContext(
                new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig);
    }

    public static Optional toProxyOptions(OidcCommonConfig.Proxy proxyConfig) {
        return OidcCommonUtils.toProxyOptions(proxyConfig);
    }

    protected static OIDCException toOidcException(Throwable cause, String authServerUrl, String tenantId) {
        final String message = OidcCommonUtils.formatConnectionErrorMessage(authServerUrl);
        LOG.warn(message);
        fireOidcServerNotAvailableEvent(authServerUrl, tenantId);
        return new OIDCException("OIDC Server is not available", cause);
    }

    protected static Uni createOidcProvider(OidcTenantConfig oidcConfig, TlsConfig tlsConfig, Vertx vertx) {
        return createOidcClientUni(oidcConfig, tlsConfig, vertx).onItem()
                .transformToUni(new Function>() {
                    @Override
                    public Uni apply(OidcProviderClient client) {
                        if (oidcConfig.jwks.resolveEarly
                                && client.getMetadata().getJsonWebKeySetUri() != null
                                && !oidcConfig.token.requireJwtIntrospectionOnly) {
                            return getJsonWebSetUni(client, oidcConfig).onItem()
                                    .transform(new Function() {
                                        @Override
                                        public OidcProvider apply(JsonWebKeySet jwks) {
                                            return new OidcProvider(client, oidcConfig, jwks,
                                                    readTokenDecryptionKey(oidcConfig));
                                        }
                                    });
                        } else {
                            return Uni.createFrom()
                                    .item(new OidcProvider(client, oidcConfig, null, readTokenDecryptionKey(oidcConfig)));
                        }
                    }
                });
    }

    private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) {
        if (oidcConfig.token.decryptionKeyLocation.isPresent()) {
            try {
                Key key = null;

                String keyContent = KeyUtils.readKeyContent(oidcConfig.token.decryptionKeyLocation.get());
                if (keyContent != null) {
                    List keys = KeyUtils.loadJsonWebKeys(keyContent);
                    if (keys != null && keys.size() == 1 &&
                            (keys.get(0).getAlgorithm() == null
                                    || keys.get(0).getAlgorithm().equals(KeyEncryptionAlgorithm.RSA_OAEP.getAlgorithm()))
                            && ("enc".equals(keys.get(0).getUse()) || keys.get(0).getUse() == null)) {
                        key = PublicJsonWebKey.class.cast(keys.get(0)).getPrivateKey();
                    }
                }
                if (key == null) {
                    key = KeyUtils.decodeDecryptionPrivateKey(keyContent);
                }
                return key;
            } catch (Exception ex) {
                throw new ConfigurationException(
                        String.format("Token decryption key for tenant %s can not be read from %s",
                                oidcConfig.tenantId.get(), oidcConfig.token.decryptionKeyLocation.get()),
                        ex);
            }
        } else {
            return null;
        }
    }

    protected static Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) {
        if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
            String tenantId = oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID);
            if (shouldFireOidcServerAvailableEvent(tenantId)) {
                return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig)
                        .invoke(new Runnable() {
                            @Override
                            public void run() {
                                fireOidcServerAvailableEvent(oidcConfig.authServerUrl.get(), tenantId);
                            }
                        });
            }
            return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig);
        } else {
            return client.getJsonWebKeySet(null);
        }
    }

    private static Uni getJsonWebSetUniWhenDiscoveryDisabled(OidcProviderClient client,
            OidcTenantConfig oidcConfig) {
        final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
        return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
                .retry()
                .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION)
                .expireIn(connectionDelayInMillisecs)
                .onFailure()
                .transform(new Function() {
                    @Override
                    public Throwable apply(Throwable t) {
                        return toOidcException(t, oidcConfig.authServerUrl.get(),
                                oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID));
                    }
                })
                .onFailure()
                .invoke(client::close);
    }

    protected static Uni createOidcClientUni(OidcTenantConfig oidcConfig,
            TlsConfig tlsConfig, Vertx vertx) {

        String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig);

        WebClientOptions options = new WebClientOptions();

        OidcCommonUtils.setHttpClientOptions(oidcConfig, tlsConfig, options);
        var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx);
        WebClient client = WebClient.create(mutinyVertx, options);

        Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters();

        Uni metadataUni = null;
        if (!oidcConfig.discoveryEnabled.orElse(true)) {
            metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString));
        } else {
            final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
            metadataUni = OidcCommonUtils
                    .discoverMetadata(client, oidcRequestFilters, authServerUriString, connectionDelayInMillisecs, mutinyVertx,
                            oidcConfig.useBlockingDnsLookup)
                    .onItem()
                    .transform(new Function() {
                        @Override
                        public OidcConfigurationMetadata apply(JsonObject json) {
                            return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString),
                                    OidcCommonUtils.getDiscoveryUri(authServerUriString));
                        }
                    });
        }
        return metadataUni.onItemOrFailure()
                .transformToUni(new BiFunction>() {

                    @Override
                    public Uni apply(OidcConfigurationMetadata metadata, Throwable t) {
                        String tenantId = oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID);
                        if (t != null) {
                            client.close();
                            return Uni.createFrom().failure(toOidcException(t, authServerUriString, tenantId));
                        }
                        if (shouldFireOidcServerAvailableEvent(tenantId)) {
                            fireOidcServerAvailableEvent(authServerUriString, tenantId);
                        }
                        if (metadata == null) {
                            client.close();
                            return Uni.createFrom().failure(new ConfigurationException(
                                    "OpenId Connect Provider configuration metadata is not configured and can not be discovered"));
                        }
                        if (oidcConfig.logout.path.isPresent()) {
                            if (!oidcConfig.endSessionPath.isPresent() && metadata.getEndSessionUri() == null) {
                                client.close();
                                return Uni.createFrom().failure(new ConfigurationException(
                                        "The application supports RP-Initiated Logout but the OpenID Provider does not advertise the end_session_endpoint"));
                            }
                        }
                        if (oidcConfig.authentication.userInfoRequired.orElse(false) && metadata.getUserInfoUri() == null) {
                            client.close();
                            return Uni.createFrom().failure(new ConfigurationException(
                                    "UserInfo is required but the OpenID Provider UserInfo endpoint is not configured."
                                            + " Use 'quarkus.oidc.user-info-path' if the discovery is disabled."));
                        }
                        return Uni.createFrom()
                                .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters));
                    }

                });
    }

    private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oidcConfig, String authServerUriString) {
        String tokenUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath);
        String introspectionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString,
                oidcConfig.introspectionPath);
        String authorizationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString,
                oidcConfig.authorizationPath);
        String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath);
        String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath);
        String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath);
        return new OidcConfigurationMetadata(tokenUri,
                introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri,
                oidcConfig.token.issuer.orElse(null));
    }

    private static void fireOidcServerNotAvailableEvent(String authServerUrl, String tenantId) {
        if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_NOT_AVAILABLE)) {
            tenantsExpectingServerAvailableEvents.add(tenantId);
        }
    }

    private static void fireOidcServerAvailableEvent(String authServerUrl, String tenantId) {
        if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_AVAILABLE)) {
            tenantsExpectingServerAvailableEvents.remove(tenantId);
        }
    }

    private static boolean shouldFireOidcServerAvailableEvent(String tenantId) {
        return tenantsExpectingServerAvailableEvents.contains(tenantId);
    }

    private static boolean fireOidcServerEvent(String authServerUrl, SecurityEvent.Type eventType) {
        if (ConfigProvider.getConfig().getOptionalValue(SECURITY_EVENTS_ENABLED_CONFIG_KEY, boolean.class).orElse(true)) {
            SecurityEventHelper.fire(
                    Arc.container().beanManager().getEvent().select(SecurityEvent.class),
                    new SecurityEvent(eventType, Map.of(AUTH_SERVER_URL, authServerUrl)));
            return true;
        }
        return false;
    }

    public Consumer createTenantResolverInterceptor(String tenantId) {
        return new Consumer() {
            @Override
            public void accept(RoutingContext routingContext) {
                routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
            }
        };
    }

    public Supplier createTenantIdentityProvider(String tenantName) {
        return new Supplier() {
            @Override
            public TenantIdentityProvider get() {
                return new TenantSpecificOidcIdentityProvider(tenantName);
            }
        };
    }

    private static final class TenantSpecificOidcIdentityProvider extends OidcIdentityProvider
            implements TenantIdentityProvider {

        private final String tenantId;
        private final BlockingSecurityExecutor blockingExecutor;

        private TenantSpecificOidcIdentityProvider(String tenantId) {
            super(Arc.container().instance(DefaultTenantConfigResolver.class).get(),
                    Arc.container().instance(BlockingSecurityExecutor.class).get());
            this.blockingExecutor = Arc.container().instance(BlockingSecurityExecutor.class).get();
            if (tenantId.equals(DEFAULT_TENANT_ID)) {
                OidcConfig config = Arc.container().instance(OidcConfig.class).get();
                this.tenantId = config.defaultTenant.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID);
            } else {
                this.tenantId = tenantId;
            }
        }

        @Override
        public Uni authenticate(AccessTokenCredential token) {
            return authenticate(new TokenAuthenticationRequest(token));
        }

        @Override
        protected Uni resolveTenantConfigContext(TokenAuthenticationRequest request,
                AuthenticationRequestContext context) {
            return tenantResolver.resolveContext(tenantId).onItem().ifNull().failWith(new Supplier() {
                @Override
                public Throwable get() {
                    return new OIDCException("Failed to resolve tenant context");
                }
            });
        }

        @Override
        protected Map getRequestData(TokenAuthenticationRequest request) {
            RoutingContext context = getRoutingContextAttribute(request);
            if (context != null) {
                return context.data();
            }
            return new HashMap<>();
        }

        private Uni authenticate(TokenAuthenticationRequest request) {
            return authenticate(request, new AuthenticationRequestContext() {
                @Override
                public Uni runBlocking(Supplier function) {
                    return blockingExecutor.executeBlocking(function);
                }
            });
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy