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

io.quarkus.oidc.runtime.CodeAuthenticationMechanism 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.OidcIdentityProvider.CODE_FLOW_ACCESS_TOKEN;
import static io.quarkus.oidc.runtime.OidcIdentityProvider.NEW_AUTHENTICATION;
import static io.quarkus.oidc.runtime.OidcIdentityProvider.REFRESH_TOKEN_GRANT_RESPONSE;

import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Permission;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

import org.jboss.logging.Logger;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.IdTokenCredential;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.Authentication;
import io.quarkus.oidc.RefreshToken;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.runtime.BlockingOperationControl;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.AuthenticationRedirectException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.impl.ServerCookie;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.impl.CookieImpl;

public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {

    static final String AMP = "&";
    static final String EQ = "=";
    static final String COOKIE_DELIM = "|";
    static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM);
    static final String SESSION_COOKIE_NAME = "q_session";
    static final String SESSION_MAX_AGE_PARAM = "session-max-age";

    private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class);

    private static final String STATE_COOKIE_NAME = "q_auth";
    private static final String POST_LOGOUT_COOKIE_NAME = "q_post_logout";

    private static QuarkusSecurityIdentity augmentIdentity(SecurityIdentity securityIdentity,
            String accessToken,
            String refreshToken,
            RoutingContext context) {
        IdTokenCredential idTokenCredential = securityIdentity.getCredential(IdTokenCredential.class);
        RefreshToken refreshTokenCredential = new RefreshToken(refreshToken);
        return QuarkusSecurityIdentity.builder()
                .setPrincipal(securityIdentity.getPrincipal())
                .addCredential(idTokenCredential)
                .addCredential(new AccessTokenCredential(accessToken, refreshTokenCredential, context))
                .addCredential(refreshTokenCredential)
                .addRoles(securityIdentity.getRoles())
                .addAttributes(securityIdentity.getAttributes())
                .addPermissionChecker(new Function>() {
                    @Override
                    public Uni apply(Permission permission) {
                        return securityIdentity.checkPermission(permission);
                    }
                }).build();
    }

    public Uni authenticate(RoutingContext context,
            IdentityProviderManager identityProviderManager) {

        final Cookie sessionCookie = context.request().getCookie(getSessionCookieName(resolver.resolveConfig(context)));

        // if session already established, try to re-authenticate
        if (sessionCookie != null) {
            Uni resolvedContext = resolver.resolveContext(context);
            return resolvedContext.onItem()
                    .transformToUni(new Function>() {
                        @Override
                        public Uni apply(TenantConfigContext tenantContext) {
                            return reAuthenticate(sessionCookie, context, identityProviderManager, tenantContext);
                        }
                    });
        }

        final String code = context.request().getParam("code");
        if (code == null) {
            return Uni.createFrom().optional(Optional.empty());
        }

        // start a new session by starting the code flow dance
        Uni resolvedContext = resolver.resolveContext(context);
        return resolvedContext.onItem().transformToUni(new Function>() {
            @Override
            public Uni apply(TenantConfigContext tenantContext) {
                return performCodeFlow(identityProviderManager, context, tenantContext, code);
            }
        });
    }

    private Uni reAuthenticate(Cookie sessionCookie,
            RoutingContext context,
            IdentityProviderManager identityProviderManager,
            TenantConfigContext configContext) {

        AuthorizationCodeTokens session = resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig,
                sessionCookie.getValue());

        context.put(CODE_FLOW_ACCESS_TOKEN, session.getAccessToken());
        return authenticate(identityProviderManager, new IdTokenCredential(session.getIdToken(), context))
                .map(new Function() {
                    @Override
                    public SecurityIdentity apply(SecurityIdentity identity) {
                        if (isLogout(context, configContext)) {
                            fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity);
                            throw redirectToLogoutEndpoint(context, configContext, session.getIdToken());
                        }

                        return augmentIdentity(identity, session.getAccessToken(), session.getRefreshToken(), context);
                    }
                }).onFailure().recoverWithUni(new Function>() {
                    @Override
                    public Uni apply(Throwable throwable) {
                        if (throwable instanceof AuthenticationRedirectException) {
                            throw AuthenticationRedirectException.class.cast(throwable);
                        }

                        if (!(throwable instanceof TokenAutoRefreshException)) {
                            Throwable cause = throwable.getCause();

                            if (cause != null && !"expired token".equalsIgnoreCase(cause.getMessage())) {
                                LOG.debugf("Authentication failure: %s", cause);
                                throw new AuthenticationCompletionException(cause);
                            }
                            if (!configContext.oidcConfig.token.refreshExpired) {
                                LOG.debug("Token has expired, token refresh is not allowed");
                                throw new AuthenticationCompletionException(cause);
                            }
                            LOG.debug("Token has expired, trying to refresh it");
                            return refreshSecurityIdentity(configContext, session.getRefreshToken(), context,
                                    identityProviderManager, false, null);
                        } else {
                            return refreshSecurityIdentity(configContext, session.getRefreshToken(), context,
                                    identityProviderManager, true,
                                    ((TokenAutoRefreshException) throwable).getSecurityIdentity());
                        }
                    }
                });

    }

    private boolean isJavaScript(RoutingContext context) {
        String value = context.request().getHeader("X-Requested-With");
        return "JavaScript".equals(value) || "XMLHttpRequest".equals(value);
    }

    // This test determines if the default behavior of returning a 302 should go forward
    // The only case that shouldn't return a 302 is if the call is a XHR and the 
    // user has set the auto direct application property to false indicating that
    // the client application will manually handle the redirect to account for SPA behavior
    private boolean shouldAutoRedirect(TenantConfigContext configContext, RoutingContext context) {
        return isJavaScript(context) ? configContext.oidcConfig.authentication.javaScriptAutoRedirect : true;
    }

    public Uni getChallenge(RoutingContext context) {

        Uni tenantContext = resolver.resolveContext(context);
        return tenantContext.onItem().transformToUni(new Function>() {
            @Override
            public Uni apply(TenantConfigContext tenantContext) {
                return getChallengeInternal(context, tenantContext);
            }
        });
    }

    public Uni getChallengeInternal(RoutingContext context, TenantConfigContext configContext) {
        removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig));

        if (!shouldAutoRedirect(configContext, context)) {
            // If the client (usually an SPA) wants to handle the redirect manually, then
            // return status code 499 and WWW-Authenticate header with the 'OIDC' value.
            return Uni.createFrom().item(new ChallengeData(499, "WWW-Authenticate", "OIDC"));
        }

        StringBuilder codeFlowParams = new StringBuilder();

        // response_type
        codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append(EQ).append(OidcConstants.CODE_FLOW_CODE);

        // client_id
        codeFlowParams.append(AMP).append(OidcConstants.CLIENT_ID).append(EQ)
                .append(urlEncode(configContext.oidcConfig.clientId.get()));

        // scope
        List scopes = new ArrayList<>();
        scopes.add("openid");
        configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll);
        codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ).append(urlEncode(String.join(" ", scopes)));

        // redirect_uri
        String redirectPath = getRedirectPath(configContext, context);
        String redirectUriParam = buildUri(context, isForceHttps(configContext), redirectPath);
        LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam);

        codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_REDIRECT_URI).append(EQ).append(urlEncode(redirectUriParam));

        // state
        codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ)
                .append(generateCodeFlowState(context, configContext, redirectPath));

        // extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests
        if (configContext.oidcConfig.authentication.getExtraParams() != null) {
            for (Map.Entry entry : configContext.oidcConfig.authentication.getExtraParams().entrySet()) {
                codeFlowParams.append(AMP).append(entry.getKey()).append(EQ).append(urlEncode(entry.getValue()));
            }
        }

        String authorizationURL = configContext.client.authorizationURL() + "?" + codeFlowParams.toString();

        return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION,
                authorizationURL));
    }

    private static String urlEncode(String value) {
        try {
            return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    private Uni performCodeFlow(IdentityProviderManager identityProviderManager,
            RoutingContext context, TenantConfigContext configContext, String code) {

        Cookie stateCookie = context.getCookie(getStateCookieName(configContext));

        String userPath = null;
        String userQuery = null;
        if (stateCookie != null) {
            List values = context.queryParam("state");
            // IDP must return a 'state' query parameter and the value of the state cookie must start with this parameter's value
            if (values.size() != 1) {
                LOG.debug("State parameter can not be empty or multi-valued");
                return Uni.createFrom().failure(new AuthenticationCompletionException());
            } else if (!stateCookie.getValue().startsWith(values.get(0))) {
                LOG.debug("State cookie value does not match the state query parameter value");
                return Uni.createFrom().failure(new AuthenticationCompletionException());
            } else {
                // This is an original redirect from IDP, check if the original request path and query need to be restored
                String[] pair = COOKIE_PATTERN.split(stateCookie.getValue());
                if (pair.length == 2) {
                    int userQueryIndex = pair[1].indexOf("?");
                    if (userQueryIndex >= 0) {
                        userPath = pair[1].substring(0, userQueryIndex);
                        if (userQueryIndex + 1 < pair[1].length()) {
                            userQuery = pair[1].substring(userQueryIndex + 1);
                        }
                    } else {
                        userPath = pair[1];
                    }
                }
                removeCookie(context, configContext, getStateCookieName(configContext));
            }
        } else {
            // State cookie must be available to minimize the risk of CSRF
            LOG.debug("The state cookie is missing after a redirect from IDP, authentication has failed");
            return Uni.createFrom().failure(new AuthenticationCompletionException());
        }

        final String finalUserPath = userPath;
        final String finalUserQuery = userQuery;

        Uni codeFlowTokensUni = getCodeFlowTokensUni(context, configContext, code);

        return codeFlowTokensUni
                .onItemOrFailure()
                .transformToUni(new BiFunction>() {
                    @Override
                    public Uni apply(final AuthorizationCodeTokens tokens, final Throwable tOuter) {

                        if (tOuter != null) {
                            LOG.debugf("Exception during the code to token exchange: %s", tOuter.getMessage());
                            return Uni.createFrom().failure(new AuthenticationCompletionException(tOuter));
                        }

                        context.put(NEW_AUTHENTICATION, Boolean.TRUE);
                        context.put(CODE_FLOW_ACCESS_TOKEN, tokens.getAccessToken());

                        return authenticate(identityProviderManager, new IdTokenCredential(tokens.getIdToken(), context))
                                .map(new Function() {
                                    @Override
                                    public SecurityIdentity apply(SecurityIdentity identity) {
                                        processSuccessfulAuthentication(context, configContext,
                                                tokens, identity);

                                        boolean removeRedirectParams = configContext.oidcConfig.authentication
                                                .isRemoveRedirectParameters();
                                        if (removeRedirectParams || finalUserPath != null
                                                || finalUserQuery != null) {

                                            URI absoluteUri = URI.create(context.request().absoluteURI());

                                            StringBuilder finalUriWithoutQuery = new StringBuilder(buildUri(context,
                                                    isForceHttps(configContext),
                                                    absoluteUri.getAuthority(),
                                                    (finalUserPath != null ? finalUserPath
                                                            : absoluteUri.getRawPath())));

                                            if (!removeRedirectParams) {
                                                finalUriWithoutQuery.append('?').append(absoluteUri.getRawQuery());
                                            }
                                            if (finalUserQuery != null) {
                                                finalUriWithoutQuery.append(!removeRedirectParams ? "" : "?");
                                                finalUriWithoutQuery.append(finalUserQuery);
                                            }
                                            String finalRedirectUri = finalUriWithoutQuery.toString();
                                            LOG.debugf("Final redirect URI: %s", finalRedirectUri);
                                            throw new AuthenticationRedirectException(finalRedirectUri);
                                        } else {
                                            return augmentIdentity(identity, tokens.getAccessToken(),
                                                    tokens.getRefreshToken(), context);
                                        }
                                    }
                                }).onFailure().transform(new Function() {
                                    @Override
                                    public Throwable apply(Throwable tInner) {
                                        if (tInner instanceof AuthenticationRedirectException) {
                                            return tInner;
                                        }
                                        return new AuthenticationCompletionException(tInner);
                                    }
                                });
                    }
                });

    }

    private void processSuccessfulAuthentication(RoutingContext context,
            TenantConfigContext configContext,
            AuthorizationCodeTokens tokens,
            SecurityIdentity securityIdentity) {
        removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig));

        JsonObject idToken = OidcUtils.decodeJwtContent(tokens.getIdToken());

        if (!idToken.containsKey("exp") || !idToken.containsKey("iat")) {
            LOG.debug("ID Token is required to contain 'exp' and 'iat' claims");
            throw new AuthenticationCompletionException();
        }
        long maxAge = idToken.getLong("exp") - idToken.getLong("iat");
        if (configContext.oidcConfig.token.lifespanGrace.isPresent()) {
            maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt();
        }
        if (configContext.oidcConfig.token.refreshExpired) {
            maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds();
        }
        context.put(SESSION_MAX_AGE_PARAM, maxAge);
        String cookieValue = resolver.getTokenStateManager()
                .createTokenState(context, configContext.oidcConfig, tokens);
        createCookie(context, configContext.oidcConfig, getSessionCookieName(configContext.oidcConfig), cookieValue, maxAge);

        fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity);
    }

    private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityIdentity) {
        if (resolver.isSecurityEventObserved()) {
            resolver.getSecurityEvent().fire(new SecurityEvent(eventType, securityIdentity));
        }
    }

    private String getRedirectPath(TenantConfigContext configContext, RoutingContext context) {
        Authentication auth = configContext.oidcConfig.getAuthentication();
        return auth.getRedirectPath().isPresent() ? auth.getRedirectPath().get() : context.request().path();
    }

    private String generateCodeFlowState(RoutingContext context, TenantConfigContext configContext,
            String redirectPath) {
        String uuid = UUID.randomUUID().toString();
        String cookieValue = uuid;

        Authentication auth = configContext.oidcConfig.getAuthentication();
        if (auth.isRestorePathAfterRedirect()) {
            String requestQuery = context.request().query();
            String requestPath = !redirectPath.equals(context.request().path()) || requestQuery != null
                    ? context.request().path()
                    : "";
            if (requestQuery != null) {
                requestPath += ("?" + requestQuery);
            }
            if (!requestPath.isEmpty()) {
                cookieValue += (COOKIE_DELIM + requestPath);
            }
        }
        createCookie(context, configContext.oidcConfig, getStateCookieName(configContext), cookieValue, 60 * 30);
        return uuid;
    }

    private String generatePostLogoutState(RoutingContext context, TenantConfigContext configContext) {
        removeCookie(context, configContext, getPostLogoutCookieName(configContext));
        return createCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext),
                UUID.randomUUID().toString(),
                60 * 30).getValue();
    }

    static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig,
            String name, String value, long maxAge) {
        ServerCookie cookie = new CookieImpl(name, value);
        cookie.setHttpOnly(true);
        cookie.setSecure(oidcConfig.authentication.cookieForceSecure || context.request().isSSL());
        cookie.setMaxAge(maxAge);
        LOG.debugf(name + " cookie 'max-age' parameter is set to %d", maxAge);
        Authentication auth = oidcConfig.getAuthentication();
        setCookiePath(context, auth, cookie);
        if (auth.cookieDomain.isPresent()) {
            cookie.setDomain(auth.getCookieDomain().get());
        }
        context.response().addCookie(cookie);
        return cookie;
    }

    static void setCookiePath(RoutingContext context, Authentication auth, ServerCookie cookie) {
        if (auth.cookiePathHeader.isPresent() && context.request().headers().contains(auth.cookiePathHeader.get())) {
            cookie.setPath(context.request().getHeader(auth.cookiePathHeader.get()));
        } else if (auth.cookiePath.isPresent()) {
            cookie.setPath(auth.getCookiePath().get());
        }
    }

    private String buildUri(RoutingContext context, boolean forceHttps, String path) {
        String authority = URI.create(context.request().absoluteURI()).getAuthority();
        return buildUri(context, forceHttps, authority, path);
    }

    private String buildUri(RoutingContext context, boolean forceHttps, String authority, String path) {
        final String scheme = forceHttps ? "https" : context.request().scheme();
        String forwardedPrefix = "";
        if (resolver.isEnableHttpForwardedPrefix()) {
            String forwardedPrefixHeader = context.request().getHeader("X-Forwarded-Prefix");
            if (forwardedPrefixHeader != null && !forwardedPrefixHeader.equals("/") && !forwardedPrefixHeader.equals("//")) {
                forwardedPrefix = forwardedPrefixHeader;
                if (forwardedPrefix.endsWith("/")) {
                    forwardedPrefix = forwardedPrefix.substring(0, forwardedPrefix.length() - 1);
                }
            }
        }
        return new StringBuilder(scheme).append("://")
                .append(authority)
                .append(forwardedPrefix)
                .append(path)
                .toString();
    }

    private void removeCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) {
        ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName);
        if (cookie != null) {
            if (SESSION_COOKIE_NAME.equals(cookieName)) {
                resolver.getTokenStateManager().deleteTokens(context, configContext.oidcConfig, cookie.getValue());
            }
            removeCookie(context, cookie, configContext.oidcConfig);
        }
    }

    static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenantConfig oidcConfig) {
        if (cookie != null) {
            cookie.setValue("");
            cookie.setMaxAge(0);
            Authentication auth = oidcConfig.getAuthentication();
            setCookiePath(context, auth, cookie);
            if (auth.cookieDomain.isPresent()) {
                cookie.setDomain(auth.cookieDomain.get());
            }
        }
    }

    private boolean isLogout(RoutingContext context, TenantConfigContext configContext) {
        Optional logoutPath = configContext.oidcConfig.logout.path;

        if (logoutPath.isPresent()) {
            return context.request().absoluteURI().equals(
                    buildUri(context, false, logoutPath.get()));
        }

        return false;
    }

    private Uni refreshSecurityIdentity(TenantConfigContext configContext, String refreshToken,
            RoutingContext context, IdentityProviderManager identityProviderManager, boolean autoRefresh,
            SecurityIdentity fallback) {

        Uni refreshedTokensUni = refreshTokensUni(configContext, refreshToken);

        return refreshedTokensUni
                .onItemOrFailure()
                .transformToUni(new BiFunction>() {
                    @Override
                    public Uni apply(final AuthorizationCodeTokens tokens, final Throwable t) {
                        return Uni.createFrom().emitter(new Consumer>() {
                            @Override
                            public void accept(UniEmitter emitter) {
                                if (t != null) {
                                    LOG.debugf("ID token refresh has failed: %s", t.getMessage());

                                    if (autoRefresh) {
                                        LOG.debug("Using the current SecurityIdentity since the ID token is still valid");
                                        emitter.complete(((TokenAutoRefreshException) t).getSecurityIdentity());
                                    } else {
                                        emitter.fail(new AuthenticationFailedException(t));
                                    }
                                } else {
                                    context.put(CODE_FLOW_ACCESS_TOKEN, tokens.getAccessToken());
                                    context.put(REFRESH_TOKEN_GRANT_RESPONSE, Boolean.TRUE);

                                    //TODO: Right now the new ID token is also verified twice - when it is decoded by Vert.x and later
                                    // in OidcIdentityProvider

                                    authenticate(identityProviderManager,
                                            new IdTokenCredential(tokens.getIdToken(), context))
                                                    .subscribe().with(new Consumer() {
                                                        @Override
                                                        public void accept(SecurityIdentity identity) {
                                                            // after a successful refresh, rebuild the identity and update the cookie
                                                            processSuccessfulAuthentication(context, configContext,
                                                                    tokens, identity);
                                                            SecurityIdentity newSecurityIdentity = augmentIdentity(identity,
                                                                    tokens.getAccessToken(), tokens.getRefreshToken(), context);

                                                            fireEvent(autoRefresh ? SecurityEvent.Type.OIDC_SESSION_REFRESHED
                                                                    : SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED,
                                                                    newSecurityIdentity);

                                                            emitter.complete(newSecurityIdentity);
                                                        }
                                                    }, new Consumer() {
                                                        @Override
                                                        public void accept(Throwable throwable) {
                                                            emitter.fail(new AuthenticationFailedException(throwable));
                                                        }
                                                    });
                                }
                            }
                        });
                    }
                });
    }

    private Uni refreshTokensUni(TenantConfigContext configContext, String refreshToken) {

        return Uni.createFrom().emitter(new Consumer>() {
            @Override
            public void accept(UniEmitter emitter) {
                if (BlockingOperationControl.isBlockingAllowed()) {
                    configContext.client.refreshToken(emitter, refreshToken);
                } else {
                    resolver.getBlockingExecutor().execute(new Runnable() {
                        @Override
                        public void run() {
                            configContext.client.refreshToken(emitter, refreshToken);
                        }
                    });
                }
            }
        });
    }

    private Uni getCodeFlowTokensUni(RoutingContext context, TenantConfigContext configContext,
            String code) {

        // Code grant request
        JsonObject params = new JsonObject();

        // 'code': the code grant value returned from IDP
        params.put("code", code);

        // 'redirect_uri': typically it must match the 'redirect_uri' query parameter which was used during the code request.
        String redirectPath = getRedirectPath(configContext, context);
        String redirectUriParam = buildUri(context, isForceHttps(configContext), redirectPath);
        LOG.debugf("Token request redirect_uri parameter: %s", redirectUriParam);
        params.put("redirect_uri", redirectUriParam);

        // Client secret has to be posted as a form parameter if OIDC requires the client_secret_post authentication
        Credentials creds = configContext.oidcConfig.getCredentials();
        if (OidcCommonUtils.isClientSecretPostAuthRequired(creds)) {
            params.put(OidcConstants.CLIENT_SECRET, creds.clientSecret.value.get());
        } else if (creds.jwt.secret.isPresent()) {
            params.put(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE);
            params.put(OidcConstants.CLIENT_ASSERTION, OidcCommonUtils.signJwt(configContext.oidcConfig));
        }

        return Uni.createFrom().emitter(new Consumer>() {
            @Override
            public void accept(UniEmitter emitter) {
                if (BlockingOperationControl.isBlockingAllowed()) {
                    configContext.client.getCodeFlowTokens(emitter, params);
                } else {
                    resolver.getBlockingExecutor().execute(new Runnable() {
                        @Override
                        public void run() {
                            configContext.client.getCodeFlowTokens(emitter, params);
                        }
                    });
                }
            }
        });
    }

    private String buildLogoutRedirectUri(TenantConfigContext configContext, String idToken, RoutingContext context) {
        String logoutPath = configContext.oidcConfig.getEndSessionPath()
                .orElse(configContext.client.getLogoutPath());
        StringBuilder logoutUri = new StringBuilder(logoutPath).append("?").append("id_token_hint=").append(idToken);

        if (configContext.oidcConfig.logout.postLogoutPath.isPresent()) {
            logoutUri.append("&post_logout_redirect_uri=").append(
                    buildUri(context, isForceHttps(configContext), configContext.oidcConfig.logout.postLogoutPath.get()));
            logoutUri.append("&state=").append(generatePostLogoutState(context, configContext));
        }

        return logoutUri.toString();
    }

    private boolean isForceHttps(TenantConfigContext configContext) {
        return configContext.oidcConfig.authentication.forceRedirectHttpsScheme;
    }

    private AuthenticationRedirectException redirectToLogoutEndpoint(RoutingContext context, TenantConfigContext configContext,
            String idToken) {
        removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig));
        return new AuthenticationRedirectException(buildLogoutRedirectUri(configContext, idToken, context));
    }

    private static String getStateCookieName(TenantConfigContext configContext) {
        String cookieSuffix = getCookieSuffix(configContext.oidcConfig.tenantId.get());
        return STATE_COOKIE_NAME + cookieSuffix;
    }

    private static String getPostLogoutCookieName(TenantConfigContext configContext) {
        String cookieSuffix = getCookieSuffix(configContext.oidcConfig.tenantId.get());
        return POST_LOGOUT_COOKIE_NAME + cookieSuffix;
    }

    private static String getSessionCookieName(OidcTenantConfig oidcConfig) {
        String cookieSuffix = getCookieSuffix(oidcConfig.tenantId.get());
        return SESSION_COOKIE_NAME + cookieSuffix;
    }

    static String getCookieSuffix(String tenantId) {
        return !"Default".equals(tenantId) ? "_" + tenantId : "";
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy