<commit_before>package io.quarkus.oidc.runtime;

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.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collections;
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 org.jose4j.jwt.consumer.ErrorCodes;
import org.jose4j.jwt.consumer.InvalidJwtException;

import io.netty.handler.codec.http.HttpResponseStatus;
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.OidcTenantConfig.Authentication.ResponseMode;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
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.vertx.http.runtime.security.ChallengeData;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.impl.CookieImpl;
import io.vertx.core.http.impl.ServerCookie;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;

public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {

    static final String AMP = "&";
    static final String EQ = "=";
    static final String UNDERSCORE = "_";
    static final String COOKIE_DELIM = "|";
    static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM);
    static final String SESSION_MAX_AGE_PARAM = "session-max-age";
    static final String STATE_COOKIE_RESTORE_PATH = "restore-path";
    static final Uni<Void> VOID_UNI = Uni.createFrom().voidItem();
    static final Integer MAX_COOKIE_VALUE_LENGTH = 4096;
    static final String NO_OIDC_COOKIES_AVAILABLE = "no_oidc_cookies";
    static final String FORM_URL_ENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded";

    private static final String INTERNAL_IDTOKEN_HEADER = "internal";
    private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class);

    private final BlockingTaskRunner<String> createTokenStateRequestContext = new BlockingTaskRunner<String>();
    private final BlockingTaskRunner<AuthorizationCodeTokens> getTokenStateRequestContext = new BlockingTaskRunner<AuthorizationCodeTokens>();
    private final SecureRandom secureRandom = new SecureRandom();

    public Uni<SecurityIdentity> authenticate(RoutingContext context,
            IdentityProviderManager identityProviderManager, OidcTenantConfig oidcTenantConfig) {
        final Cookie sessionCookie = context.request().getCookie(getSessionCookieName(oidcTenantConfig));

        // if the session is already established then try to re-authenticate
        if (sessionCookie != null) {
            context.put(OidcUtils.SESSION_COOKIE_NAME, sessionCookie.getName());
            Uni<TenantConfigContext> resolvedContext = resolver.resolveContext(context);
            return resolvedContext.onItem()
                    .transformToUni(new Function<TenantConfigContext, Uni<? extends SecurityIdentity>>() {
                        @Override
                        public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
                            return reAuthenticate(sessionCookie, context, identityProviderManager, tenantContext);
                        }
                    });
        }

        final Cookie stateCookie = context.request().getCookie(getStateCookieName(oidcTenantConfig));

        // if the state cookie is available then try to complete the code flow and start a new session
        if (stateCookie != null) {
            if (ResponseMode.FORM_POST == oidcTenantConfig.authentication.responseMode.orElse(ResponseMode.QUERY)) {
                String contentType = context.request().getHeader("Content-Type");
                if (context.request().method() == HttpMethod.POST
                        && contentType != null
                        && (contentType.equals(FORM_URL_ENCODED_CONTENT_TYPE)
                                || contentType.startsWith(FORM_URL_ENCODED_CONTENT_TYPE + ";"))) {
                    context.request().setExpectMultipart(true);
                    return Uni.createFrom().emitter(new Consumer<UniEmitter<? super MultiMap>>() {
                        @Override
                        public void accept(UniEmitter<? super MultiMap> t) {
                            context.request().endHandler(new Handler<Void>() {
                                @Override
                                public void handle(Void event) {
                                    t.complete(context.request().formAttributes());
                                }
                            });
                            context.request().resume();
                        }
                    }).onItem().transformToUni(new Function<MultiMap, Uni<? extends SecurityIdentity>>() {
                        @Override
                        public Uni<? extends SecurityIdentity> apply(MultiMap requestParams) {
                            return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager, stateCookie,
                                    requestParams);
                        }
                    });
                }
                LOG.debug("HTTP POST and " + FORM_URL_ENCODED_CONTENT_TYPE
                        + " content type must be used with the form_post response mode");
                return Uni.createFrom().failure(new AuthenticationFailedException());
            } else {
                return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager, stateCookie,
                        context.queryParams());
            }
        }
        // return an empty identity - this will lead to a challenge redirecting the user to OpenId Connect provider
        // unless it is detected it is a redirect from the provider in which case HTTP 401 will be returned.
        context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE);
        return Uni.createFrom().optional(Optional.empty());

    }

    private Uni<SecurityIdentity> processRedirectFromOidc(RoutingContext context, OidcTenantConfig oidcTenantConfig,
            IdentityProviderManager identityProviderManager, Cookie stateCookie, MultiMap requestParams) {
        String[] parsedStateCookieValue = COOKIE_PATTERN.split(stateCookie.getValue());
        OidcUtils.removeCookie(context, oidcTenantConfig, stateCookie.getName());

        if (!isStateValid(requestParams, parsedStateCookieValue[0])) {
            return Uni.createFrom().failure(new AuthenticationCompletionException());
        }

        if (requestParams.contains(OidcConstants.CODE_FLOW_CODE)) {
            // start a new session by starting the code flow dance
            Uni<TenantConfigContext> resolvedContext = resolver.resolveContext(context);
            return resolvedContext.onItem()
                    .transformToUni(new Function<TenantConfigContext, Uni<? extends SecurityIdentity>>() {
                        @Override
                        public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
                            return performCodeFlow(identityProviderManager, context, tenantContext, requestParams,
                                    parsedStateCookieValue);
                        }
                    });
        } else if (requestParams.contains(OidcConstants.CODE_FLOW_ERROR)) {
            OidcUtils.removeCookie(context, oidcTenantConfig, stateCookie.getName());
            String error = requestParams.get(OidcConstants.CODE_FLOW_ERROR);
            String errorDescription = requestParams.get(OidcConstants.CODE_FLOW_ERROR_DESCRIPTION);

            LOG.debugf("Authentication has failed, error: %s, description: %s", error, errorDescription);

            if (oidcTenantConfig.authentication.errorPath.isPresent()) {
                URI absoluteUri = URI.create(context.request().absoluteURI());

                StringBuilder errorUri = new StringBuilder(buildUri(context,
                        isForceHttps(oidcTenantConfig),
                        absoluteUri.getAuthority(),
                        oidcTenantConfig.authentication.errorPath.get()));
                errorUri.append('?').append(getRequestParametersAsQuery(absoluteUri, requestParams, oidcTenantConfig));

                String finalErrorUri = errorUri.toString();
                LOG.debugf("Error URI: %s", finalErrorUri);
                return Uni.createFrom().failure(new AuthenticationRedirectException(finalErrorUri));
            } else {
                return Uni.createFrom().failure(new AuthenticationCompletionException());
            }
        } else {
            LOG.debug("State cookie is present but neither 'code' nor 'error' query parameter is returned");
            return Uni.createFrom().failure(new AuthenticationCompletionException());
        }

    }

    private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParams, OidcTenantConfig oidcConfig) {
        if (ResponseMode.FORM_POST == oidcConfig.authentication.responseMode.orElse(ResponseMode.QUERY)) {
            return OidcCommonUtils.encodeForm(new io.vertx.mutiny.core.MultiMap(requestParams)).toString();
        } else {
            return requestUri.getRawQuery();
        }
    }

    private boolean isStateValid(MultiMap requestParams, String cookieState) {
        List<String> values = requestParams.getAll(OidcConstants.CODE_FLOW_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 false;
        } else if (!cookieState.equals(values.get(0))) {
            LOG.debug("State cookie value does not match the state query parameter value");
            return false;
        }
        return true;
    }

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

        context.put(TenantConfigContext.class.getName(), configContext);
        return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig,
                sessionCookie.getValue(), getTokenStateRequestContext)
                .chain(new Function<AuthorizationCodeTokens, Uni<? extends SecurityIdentity>>() {
                    @Override
                    public Uni<? extends SecurityIdentity> apply(AuthorizationCodeTokens session) {
                        context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken());
                        context.put(AuthorizationCodeTokens.class.getName(), session);
                        return authenticate(identityProviderManager, context,
                                new IdTokenCredential(session.getIdToken(),
                                        isInternalIdToken(session.getIdToken(), configContext)))
                                                .call(new Function<SecurityIdentity, Uni<?>>() {
                                                    @Override
                                                    public Uni<Void> apply(SecurityIdentity identity) {
                                                        if (isLogout(context, configContext)) {
                                                            fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity);
                                                            return buildLogoutRedirectUriUni(context, configContext,
                                                                    session.getIdToken());
                                                        }
                                                        return VOID_UNI;
                                                    }
                                                }).onFailure()
                                                .recoverWithUni(new Function<Throwable, Uni<? extends SecurityIdentity>>() {
                                                    @Override
                                                    public Uni<? extends SecurityIdentity> apply(Throwable t) {
                                                        if (t instanceof AuthenticationRedirectException) {
                                                            throw (AuthenticationRedirectException) t;
                                                        }

                                                        if (!(t instanceof TokenAutoRefreshException)) {
                                                            boolean expired = (t.getCause() instanceof InvalidJwtException)
                                                                    && ((InvalidJwtException) t.getCause())
                                                                            .hasErrorCode(ErrorCodes.EXPIRED);

                                                            if (!expired) {
                                                                LOG.debugf("Authentication failure: %s", t.getCause());
                                                                throw new AuthenticationCompletionException(t.getCause());
                                                            }
                                                            if (!configContext.oidcConfig.token.refreshExpired) {
                                                                LOG.debug("Token has expired, token refresh is not allowed");
                                                                throw new AuthenticationCompletionException(t.getCause());
                                                            }
                                                            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) t).getSecurityIdentity());
                                                        }
                                                    }
                                                });
                    }

                });
    }

    private boolean isInternalIdToken(String idToken, TenantConfigContext configContext) {
        if (!configContext.oidcConfig.authentication.idTokenRequired.orElse(true)) {
            JsonObject headers = OidcUtils.decodeJwtHeaders(idToken);
            if (headers != null) {
                return headers.getBoolean(INTERNAL_IDTOKEN_HEADER, false);
            }
        }
        return false;
    }

    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<ChallengeData> getChallenge(RoutingContext context) {

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

    public Uni<ChallengeData> getChallengeInternal(RoutingContext context, TenantConfigContext configContext) {
        return removeSessionCookie(context, configContext.oidcConfig)
                .chain(new Function<Void, Uni<? extends ChallengeData>>() {

                    @Override
                    public Uni<ChallengeData> apply(Void t) {

                        if (context.get(NO_OIDC_COOKIES_AVAILABLE) != null
                                && isRedirectFromProvider(context, configContext)) {
                            LOG.debug(
                                    "The state cookie is missing after the redirect from OpenId Connect Provider, authentication has failed");
                            return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "OIDC"));
                        }

                        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(168); // experimentally determined to be a good size for preventing resizing and not wasting space

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

                        // response_mode
                        if (ResponseMode.FORM_POST == configContext.oidcConfig.authentication.responseMode
                                .orElse(ResponseMode.QUERY)) {
                            codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_MODE).append(EQ)
                                    .append(configContext.oidcConfig.authentication.responseMode.get().toString()
                                            .toLowerCase());
                        }

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

                        // scope
                        List<String> oidcConfigScopes = configContext.oidcConfig.getAuthentication().scopes.isPresent()
                                ? configContext.oidcConfig.getAuthentication().scopes.get()
                                : Collections.emptyList();<commit_msg>The OIDC Response mode is corrected in the query string when specified<commit_after>package io.quarkus.oidc.runtime;

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.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collections;
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 org.jose4j.jwt.consumer.ErrorCodes;
import org.jose4j.jwt.consumer.InvalidJwtException;

import io.netty.handler.codec.http.HttpResponseStatus;
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.OidcTenantConfig.Authentication.ResponseMode;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
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.vertx.http.runtime.security.ChallengeData;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.impl.CookieImpl;
import io.vertx.core.http.impl.ServerCookie;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;

public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {

    static final String AMP = "&";
    static final String EQ = "=";
    static final String UNDERSCORE = "_";
    static final String COOKIE_DELIM = "|";
    static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM);
    static final String SESSION_MAX_AGE_PARAM = "session-max-age";
    static final String STATE_COOKIE_RESTORE_PATH = "restore-path";
    static final Uni<Void> VOID_UNI = Uni.createFrom().voidItem();
    static final Integer MAX_COOKIE_VALUE_LENGTH = 4096;
    static final String NO_OIDC_COOKIES_AVAILABLE = "no_oidc_cookies";
    static final String FORM_URL_ENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded";

    private static final String INTERNAL_IDTOKEN_HEADER = "internal";
    private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class);

    private final BlockingTaskRunner<String> createTokenStateRequestContext = new BlockingTaskRunner<String>();
    private final BlockingTaskRunner<AuthorizationCodeTokens> getTokenStateRequestContext = new BlockingTaskRunner<AuthorizationCodeTokens>();
    private final SecureRandom secureRandom = new SecureRandom();

    public Uni<SecurityIdentity> authenticate(RoutingContext context,
            IdentityProviderManager identityProviderManager, OidcTenantConfig oidcTenantConfig) {
        final Cookie sessionCookie = context.request().getCookie(getSessionCookieName(oidcTenantConfig));

        // if the session is already established then try to re-authenticate
        if (sessionCookie != null) {
            context.put(OidcUtils.SESSION_COOKIE_NAME, sessionCookie.getName());
            Uni<TenantConfigContext> resolvedContext = resolver.resolveContext(context);
            return resolvedContext.onItem()
                    .transformToUni(new Function<TenantConfigContext, Uni<? extends SecurityIdentity>>() {
                        @Override
                        public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
                            return reAuthenticate(sessionCookie, context, identityProviderManager, tenantContext);
                        }
                    });
        }

        final Cookie stateCookie = context.request().getCookie(getStateCookieName(oidcTenantConfig));

        // if the state cookie is available then try to complete the code flow and start a new session
        if (stateCookie != null) {
            if (ResponseMode.FORM_POST == oidcTenantConfig.authentication.responseMode.orElse(ResponseMode.QUERY)) {
                String contentType = context.request().getHeader("Content-Type");
                if (context.request().method() == HttpMethod.POST
                        && contentType != null
                        && (contentType.equals(FORM_URL_ENCODED_CONTENT_TYPE)
                                || contentType.startsWith(FORM_URL_ENCODED_CONTENT_TYPE + ";"))) {
                    context.request().setExpectMultipart(true);
                    return Uni.createFrom().emitter(new Consumer<UniEmitter<? super MultiMap>>() {
                        @Override
                        public void accept(UniEmitter<? super MultiMap> t) {
                            context.request().endHandler(new Handler<Void>() {
                                @Override
                                public void handle(Void event) {
                                    t.complete(context.request().formAttributes());
                                }
                            });
                            context.request().resume();
                        }
                    }).onItem().transformToUni(new Function<MultiMap, Uni<? extends SecurityIdentity>>() {
                        @Override
                        public Uni<? extends SecurityIdentity> apply(MultiMap requestParams) {
                            return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager, stateCookie,
                                    requestParams);
                        }
                    });
                }
                LOG.debug("HTTP POST and " + FORM_URL_ENCODED_CONTENT_TYPE
                        + " content type must be used with the form_post response mode");
                return Uni.createFrom().failure(new AuthenticationFailedException());
            } else {
                return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager, stateCookie,
                        context.queryParams());
            }
        }
        // return an empty identity - this will lead to a challenge redirecting the user to OpenId Connect provider
        // unless it is detected it is a redirect from the provider in which case HTTP 401 will be returned.
        context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE);
        return Uni.createFrom().optional(Optional.empty());

    }

    private Uni<SecurityIdentity> processRedirectFromOidc(RoutingContext context, OidcTenantConfig oidcTenantConfig,
            IdentityProviderManager identityProviderManager, Cookie stateCookie, MultiMap requestParams) {
        String[] parsedStateCookieValue = COOKIE_PATTERN.split(stateCookie.getValue());
        OidcUtils.removeCookie(context, oidcTenantConfig, stateCookie.getName());

        if (!isStateValid(requestParams, parsedStateCookieValue[0])) {
            return Uni.createFrom().failure(new AuthenticationCompletionException());
        }

        if (requestParams.contains(OidcConstants.CODE_FLOW_CODE)) {
            // start a new session by starting the code flow dance
            Uni<TenantConfigContext> resolvedContext = resolver.resolveContext(context);
            return resolvedContext.onItem()
                    .transformToUni(new Function<TenantConfigContext, Uni<? extends SecurityIdentity>>() {
                        @Override
                        public Uni<SecurityIdentity> apply(TenantConfigContext tenantContext) {
                            return performCodeFlow(identityProviderManager, context, tenantContext, requestParams,
                                    parsedStateCookieValue);
                        }
                    });
        } else if (requestParams.contains(OidcConstants.CODE_FLOW_ERROR)) {
            OidcUtils.removeCookie(context, oidcTenantConfig, stateCookie.getName());
            String error = requestParams.get(OidcConstants.CODE_FLOW_ERROR);
            String errorDescription = requestParams.get(OidcConstants.CODE_FLOW_ERROR_DESCRIPTION);

            LOG.debugf("Authentication has failed, error: %s, description: %s", error, errorDescription);

            if (oidcTenantConfig.authentication.errorPath.isPresent()) {
                URI absoluteUri = URI.create(context.request().absoluteURI());

                StringBuilder errorUri = new StringBuilder(buildUri(context,
                        isForceHttps(oidcTenantConfig),
                        absoluteUri.getAuthority(),
                        oidcTenantConfig.authentication.errorPath.get()));
                errorUri.append('?').append(getRequestParametersAsQuery(absoluteUri, requestParams, oidcTenantConfig));

                String finalErrorUri = errorUri.toString();
                LOG.debugf("Error URI: %s", finalErrorUri);
                return Uni.createFrom().failure(new AuthenticationRedirectException(finalErrorUri));
            } else {
                return Uni.createFrom().failure(new AuthenticationCompletionException());
            }
        } else {
            LOG.debug("State cookie is present but neither 'code' nor 'error' query parameter is returned");
            return Uni.createFrom().failure(new AuthenticationCompletionException());
        }

    }

    private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParams, OidcTenantConfig oidcConfig) {
        if (ResponseMode.FORM_POST == oidcConfig.authentication.responseMode.orElse(ResponseMode.QUERY)) {
            return OidcCommonUtils.encodeForm(new io.vertx.mutiny.core.MultiMap(requestParams)).toString();
        } else {
            return requestUri.getRawQuery();
        }
    }

    private boolean isStateValid(MultiMap requestParams, String cookieState) {
        List<String> values = requestParams.getAll(OidcConstants.CODE_FLOW_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 false;
        } else if (!cookieState.equals(values.get(0))) {
            LOG.debug("State cookie value does not match the state query parameter value");
            return false;
        }
        return true;
    }

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

        context.put(TenantConfigContext.class.getName(), configContext);
        return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig,
                sessionCookie.getValue(), getTokenStateRequestContext)
                .chain(new Function<AuthorizationCodeTokens, Uni<? extends SecurityIdentity>>() {
                    @Override
                    public Uni<? extends SecurityIdentity> apply(AuthorizationCodeTokens session) {
                        context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken());
                        context.put(AuthorizationCodeTokens.class.getName(), session);
                        return authenticate(identityProviderManager, context,
                                new IdTokenCredential(session.getIdToken(),
                                        isInternalIdToken(session.getIdToken(), configContext)))
                                                .call(new Function<SecurityIdentity, Uni<?>>() {
                                                    @Override
                                                    public Uni<Void> apply(SecurityIdentity identity) {
                                                        if (isLogout(context, configContext)) {
                                                            fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity);
                                                            return buildLogoutRedirectUriUni(context, configContext,
                                                                    session.getIdToken());
                                                        }
                                                        return VOID_UNI;
                                                    }
                                                }).onFailure()
                                                .recoverWithUni(new Function<Throwable, Uni<? extends SecurityIdentity>>() {
                                                    @Override
                                                    public Uni<? extends SecurityIdentity> apply(Throwable t) {
                                                        if (t instanceof AuthenticationRedirectException) {
                                                            throw (AuthenticationRedirectException) t;
                                                        }

                                                        if (!(t instanceof TokenAutoRefreshException)) {
                                                            boolean expired = (t.getCause() instanceof InvalidJwtException)
                                                                    && ((InvalidJwtException) t.getCause())
                                                                            .hasErrorCode(ErrorCodes.EXPIRED);

                                                            if (!expired) {
                                                                LOG.debugf("Authentication failure: %s", t.getCause());
                                                                throw new AuthenticationCompletionException(t.getCause());
                                                            }
                                                            if (!configContext.oidcConfig.token.refreshExpired) {
                                                                LOG.debug("Token has expired, token refresh is not allowed");
                                                                throw new AuthenticationCompletionException(t.getCause());
                                                            }
                                                            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) t).getSecurityIdentity());
                                                        }
                                                    }
                                                });
                    }

                });
    }

    private boolean isInternalIdToken(String idToken, TenantConfigContext configContext) {
        if (!configContext.oidcConfig.authentication.idTokenRequired.orElse(true)) {
            JsonObject headers = OidcUtils.decodeJwtHeaders(idToken);
            if (headers != null) {
                return headers.getBoolean(INTERNAL_IDTOKEN_HEADER, false);
            }
        }
        return false;
    }

    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<ChallengeData> getChallenge(RoutingContext context) {

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

    public Uni<ChallengeData> getChallengeInternal(RoutingContext context, TenantConfigContext configContext) {
        return removeSessionCookie(context, configContext.oidcConfig)
                .chain(new Function<Void, Uni<? extends ChallengeData>>() {

                    @Override
                    public Uni<ChallengeData> apply(Void t) {

                        if (context.get(NO_OIDC_COOKIES_AVAILABLE) != null
                                && isRedirectFromProvider(context, configContext)) {
                            LOG.debug(
                                    "The state cookie is missing after the redirect from OpenId Connect Provider, authentication has failed");
                            return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "OIDC"));
                        }

                        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(168); // experimentally determined to be a good size for preventing resizing and not wasting space

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

                        // response_mode
                        if (ResponseMode.FORM_POST == configContext.oidcConfig.authentication.responseMode
                                .orElse(ResponseMode.QUERY)) {
                            codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_RESPONSE_MODE).append(EQ)
                                    .append(configContext.oidcConfig.authentication.responseMode.get().toString()
                                            .toLowerCase());
                        }

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

                        // scope
                        List<String> oidcConfigScopes = configContext.oidcConfig.getAuthentication().scopes.isPresent()
                                ? configContext.oidcConfig.getAuthentication().scopes.get()
                                : Collections.emptyList();
{'fraction_non_alphanumeric': 0.07610937234636193, 'fraction_numerical': 0.0016498046922386394, 'mean_word_length': 1.7553980881074938, 'pattern_counts': {'":': 0, '<': 89, '<?xml version=': 0, '>': 89, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 204, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3233576', 'n_tokens_mistral': 9268, 'n_tokens_neox': 8849, 'n_words': 1906}