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

io.quarkus.oidc.runtime.OidcRecorder 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 java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jboss.logging.Logger;

import io.quarkus.arc.Arc;
import io.quarkus.oidc.OIDCException;
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.common.runtime.OidcCommonConfig;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.runtime.BlockingOperationControl;
import io.quarkus.runtime.ExecutorRecorder;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.vertx.core.Vertx;
import io.vertx.core.net.ProxyOptions;
import io.vertx.ext.auth.oauth2.OAuth2ClientOptions;
import io.vertx.ext.jwt.JWTOptions;

@Recorder
public class OidcRecorder {

    private static final Logger LOG = Logger.getLogger(OidcRecorder.class);
    private static final String DEFAULT_TENANT_ID = "Default";

    private static final Map dynamicTenantsConfig = new ConcurrentHashMap<>();

    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 = createTenantContext(vertxValue, config.defaultTenant, 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(),
                    createTenantContext(vertxValue, tenant.getValue(), 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 Uni.createFrom().emitter(new Consumer>() {
                                    @Override
                                    public void accept(UniEmitter uniEmitter) {
                                        if (BlockingOperationControl.isBlockingAllowed()) {
                                            createDynamicTenantContext(uniEmitter, vertxValue, config, tlsConfig,
                                                    config.getTenantId().get());
                                        } else {
                                            ExecutorRecorder.getCurrent().execute(new Runnable() {
                                                @Override
                                                public void run() {
                                                    createDynamicTenantContext(uniEmitter, vertxValue, config, tlsConfig,
                                                            config.getTenantId().get());
                                                }
                                            });
                                        }
                                    }
                                });

                            }
                        },
                        ExecutorRecorder.getCurrent());
            }
        };
    }

    private void createDynamicTenantContext(UniEmitter uniEmitter, Vertx vertx,
            OidcTenantConfig oidcConfig, TlsConfig tlsConfig, String tenantId) {
        try {
            if (!dynamicTenantsConfig.containsKey(tenantId)) {
                dynamicTenantsConfig.putIfAbsent(tenantId, createTenantContext(vertx, oidcConfig, tlsConfig, tenantId));
            }
            uniEmitter.complete(dynamicTenantsConfig.get(tenantId));
        } catch (Throwable t) {
            uniEmitter.fail(t);
        }
    }

    private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oidcConfig, TlsConfig tlsConfig,
            String tenantId) {
        if (!oidcConfig.tenantId.isPresent()) {
            oidcConfig.tenantId = Optional.of(tenantId);
        }
        if (!oidcConfig.tenantEnabled) {
            LOG.debugf("'%s' tenant configuration is disabled", tenantId);
            return new TenantConfigContext(new OidcRuntimeClient(null), oidcConfig);
        }

        OAuth2ClientOptions options = new OAuth2ClientOptions();

        if (oidcConfig.getClientId().isPresent()) {
            options.setClientID(oidcConfig.getClientId().get());
        }

        if (oidcConfig.getToken().issuer.isPresent()) {
            options.setValidateIssuer(false);
        }

        if (oidcConfig.getToken().getLifespanGrace().isPresent()) {
            JWTOptions jwtOptions = new JWTOptions();
            jwtOptions.setLeeway(oidcConfig.getToken().getLifespanGrace().getAsInt());
            options.setJWTOptions(jwtOptions);
        }

        if (oidcConfig.getPublicKey().isPresent()) {
            return createdTenantContextFromPublicKey(options, oidcConfig);
        }

        OidcCommonUtils.verifyCommonConfiguration(oidcConfig);

        // Base IDP server URL
        String authServerUrl = OidcCommonUtils.getAuthServerUrl(oidcConfig);
        options.setSite(authServerUrl);

        if (!oidcConfig.discoveryEnabled) {
            if (oidcConfig.applicationType != ApplicationType.SERVICE) {
                if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) {
                    throw new OIDCException("'web-app' applications must have 'authorization-path' and 'token-path' properties "
                            + "set when the discovery is disabled.");
                }
                // These endpoints can only be used with the code flow
                options.setAuthorizationPath(OidcCommonUtils.getOidcEndpointUrl(authServerUrl, oidcConfig.authorizationPath));
                options.setTokenPath(OidcCommonUtils.getOidcEndpointUrl(authServerUrl, oidcConfig.tokenPath));
            }

            if (oidcConfig.getUserInfoPath().isPresent()) {
                options.setUserInfoPath(OidcCommonUtils.getOidcEndpointUrl(authServerUrl, oidcConfig.userInfoPath));
            }

            // JWK and introspection endpoints have to be set for both 'web-app' and 'service' applications  
            if (!oidcConfig.jwksPath.isPresent() && !oidcConfig.introspectionPath.isPresent()) {
                throw new OIDCException(
                        "Either 'jwks-path' or 'introspection-path' properties must be set when the discovery is disabled.");
            }

            if (oidcConfig.getIntrospectionPath().isPresent()) {
                options.setIntrospectionPath(OidcCommonUtils.getOidcEndpointUrl(authServerUrl, oidcConfig.introspectionPath));
            }

            if (oidcConfig.getJwksPath().isPresent()) {
                options.setJwkPath(OidcCommonUtils.getOidcEndpointUrl(authServerUrl, oidcConfig.jwksPath));
            }

        }

        if (ApplicationType.SERVICE.equals(oidcConfig.applicationType)) {
            if (oidcConfig.token.refreshExpired) {
                throw new ConfigurationException(
                        "The 'token.refresh-expired' 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");
            }
        }

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

            if (oidcConfig.authentication.userInfoRequired || 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");
            }
        }

        // TODO: The workaround to support client_secret_post is added below and have to be removed once
        // it is supported again in VertX OAuth2.
        Credentials creds = oidcConfig.getCredentials();
        if (OidcCommonUtils.isClientSecretBasicAuthRequired(creds)) {
            // If it is set for client_secret_post as well then VertX OAuth2 will only use client_secret_basic
            options.setClientSecret(OidcCommonUtils.clientSecret(creds));
        } else {
            // Avoid VertX OAuth2 setting a null client_secret form parameter if it is client_secret_post or client_secret_jwt
            options.setClientSecretParameterName(null);
        }

        OidcCommonUtils.setHttpClientOptions(oidcConfig, tlsConfig, options);

        final long connectionRetryCount = OidcCommonUtils.getConnectionRetryCount(oidcConfig);
        if (connectionRetryCount > 1) {
            LOG.infof("Connecting to IDP for up to %d times every 2 seconds", connectionRetryCount);
        }

        OidcRuntimeClient client = null;
        for (long i = 0; i < connectionRetryCount; i++) {
            try {
                if (oidcConfig.discoveryEnabled) {
                    client = OidcRuntimeClient.discoverOidcEndpoints(vertx, options, oidcConfig);
                } else {
                    client = OidcRuntimeClient.setOidcEndpoints(vertx, options, oidcConfig);
                }

                break;
            } catch (Throwable throwable) {
                while (throwable instanceof CompletionException && throwable.getCause() != null) {
                    throwable = throwable.getCause();
                }
                if (throwable instanceof OIDCException) {
                    if (i + 1 < connectionRetryCount) {
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException iex) {
                            // continue connecting
                        }
                    } else {
                        throw (OIDCException) throwable;
                    }
                } else {
                    throw new OIDCException(throwable);
                }
            }
        }

        if (oidcConfig.logout.path.isPresent()) {
            if (!oidcConfig.endSessionPath.isPresent() && client.getLogoutPath() == null) {
                throw new RuntimeException(
                        "The application supports RP-Initiated Logout but the OpenID Provider does not advertise the end_session_endpoint");
            }
        }

        return new TenantConfigContext(client, oidcConfig);
    }

    private static TenantConfigContext createdTenantContextFromPublicKey(OAuth2ClientOptions options,
            OidcTenantConfig oidcConfig) {
        if (oidcConfig.applicationType != ApplicationType.SERVICE) {
            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(OidcRuntimeClient.createClientWithPublicKey(options, oidcConfig.publicKey.get()),
                oidcConfig);
    }

    public void setSecurityEventObserved(boolean isSecurityEventObserved) {
        DefaultTenantConfigResolver bean = Arc.container().instance(DefaultTenantConfigResolver.class).get();
        bean.setSecurityEventObserved(isSecurityEventObserved);
    }

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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy