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

io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism Maven / Gradle / Ivy

The newest version!
package io.quarkus.vertx.http.runtime.security;

import static io.quarkus.security.spi.runtime.SecurityEventHelper.fire;
import static io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent.createLoginEvent;

import java.net.URI;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;

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

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.credential.PasswordCredential;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.FormAuthConfig;
import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
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.CookieSameSite;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;

public class FormAuthenticationMechanism implements HttpAuthenticationMechanism {
    private static final String FORM = "form";

    private static final Logger log = Logger.getLogger(FormAuthenticationMechanism.class);

    private final String loginPage;
    private final String errorPage;
    private final String postLocation;
    private final String usernameParameter;
    private final String passwordParameter;
    private final String locationCookie;
    private final String landingPage;
    private final boolean redirectToLandingPage;
    private final boolean redirectToErrorPage;
    private final boolean redirectToLoginPage;
    private final CookieSameSite cookieSameSite;
    private final String cookiePath;
    private final boolean isFormAuthEventObserver;
    private final PersistentLoginManager loginManager;
    private final Event formAuthEvent;

    //the temp encryption key, persistent across dev mode restarts
    static volatile String encryptionKey;

    @Inject
    FormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig,
            Event formAuthEvent, BeanManager beanManager,
            @ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) {
        String key;
        if (httpConfiguration.encryptionKey.isEmpty()) {
            if (encryptionKey != null) {
                //persist across dev mode restarts
                key = encryptionKey;
            } else {
                byte[] data = new byte[32];
                new SecureRandom().nextBytes(data);
                key = encryptionKey = Base64.getEncoder().encodeToString(data);
                log.warn("Encryption key was not specified for persistent FORM auth, using temporary key " + key);
            }
        } else {
            key = httpConfiguration.encryptionKey.get();
        }
        FormAuthConfig form = buildTimeConfig.auth.form;
        FormAuthRuntimeConfig runtimeForm = httpConfiguration.auth.form;
        this.loginManager = new PersistentLoginManager(key, runtimeForm.cookieName, runtimeForm.timeout.toMillis(),
                runtimeForm.newCookieInterval.toMillis(), runtimeForm.httpOnlyCookie, runtimeForm.cookieSameSite.name(),
                runtimeForm.cookiePath.orElse(null), runtimeForm.cookieMaxAge.map(Duration::toSeconds).orElse(-1L));
        this.loginPage = startWithSlash(runtimeForm.loginPage.orElse(null));
        this.errorPage = startWithSlash(runtimeForm.errorPage.orElse(null));
        this.landingPage = startWithSlash(runtimeForm.landingPage.orElse(null));
        this.postLocation = startWithSlash(form.postLocation);
        this.usernameParameter = runtimeForm.usernameParameter;
        this.passwordParameter = runtimeForm.passwordParameter;
        this.locationCookie = runtimeForm.locationCookie;
        this.cookiePath = runtimeForm.cookiePath.orElse(null);
        boolean redirectAfterLogin = runtimeForm.redirectAfterLogin;
        this.redirectToLandingPage = landingPage != null && redirectAfterLogin;
        this.redirectToLoginPage = loginPage != null;
        this.redirectToErrorPage = errorPage != null;
        this.cookieSameSite = CookieSameSite.valueOf(runtimeForm.cookieSameSite.name());
        this.isFormAuthEventObserver = SecurityEventHelper.isEventObserved(createLoginEvent(null), beanManager,
                securityEventsEnabled);
        this.formAuthEvent = this.isFormAuthEventObserver ? formAuthEvent : null;
    }

    public FormAuthenticationMechanism(String loginPage, String postLocation,
            String usernameParameter, String passwordParameter, String errorPage, String landingPage,
            boolean redirectAfterLogin, String locationCookie, String cookieSameSite, String cookiePath,
            PersistentLoginManager loginManager) {
        this.loginPage = loginPage;
        this.postLocation = postLocation;
        this.usernameParameter = usernameParameter;
        this.passwordParameter = passwordParameter;
        this.locationCookie = locationCookie;
        this.errorPage = errorPage;
        this.landingPage = landingPage;
        this.redirectToLandingPage = landingPage != null && redirectAfterLogin;
        this.redirectToLoginPage = loginPage != null;
        this.redirectToErrorPage = errorPage != null;
        this.cookieSameSite = CookieSameSite.valueOf(cookieSameSite);
        this.cookiePath = cookiePath;
        this.loginManager = loginManager;
        this.isFormAuthEventObserver = false;
        this.formAuthEvent = null;
    }

    public Uni runFormAuth(final RoutingContext exchange,
            final IdentityProviderManager securityContext) {
        exchange.request().setExpectMultipart(true);
        return Uni.createFrom().emitter(new Consumer>() {
            @Override
            public void accept(UniEmitter uniEmitter) {
                exchange.request().endHandler(new Handler() {
                    @Override
                    public void handle(Void event) {
                        try {
                            MultiMap res = exchange.request().formAttributes();

                            final String jUsername = res.get(usernameParameter);
                            final String jPassword = res.get(passwordParameter);
                            if (jUsername == null || jPassword == null) {
                                log.debugf(
                                        "Could not authenticate as username or password was not present in the posted result for %s",
                                        exchange);
                                uniEmitter.complete(null);
                                return;
                            }
                            securityContext
                                    .authenticate(HttpSecurityUtils
                                            .setRoutingContextAttribute(new UsernamePasswordAuthenticationRequest(jUsername,
                                                    new PasswordCredential(jPassword.toCharArray())), exchange))
                                    .subscribe().with(new Consumer() {
                                        @Override
                                        public void accept(SecurityIdentity identity) {
                                            if (isFormAuthEventObserver) {
                                                fire(formAuthEvent, createLoginEvent(identity));
                                            }

                                            try {
                                                loginManager.save(identity, exchange, null, exchange.request().isSSL());
                                                if (redirectToLandingPage
                                                        || exchange.request().getCookie(locationCookie) != null) {
                                                    handleRedirectBack(exchange);
                                                    //we  have authenticated, but we want to just redirect back to the original page
                                                    //so we don't actually authenticate the current request
                                                    //instead we have just set a cookie so the redirected request will be authenticated
                                                } else {
                                                    exchange.response().setStatusCode(200);
                                                    exchange.response().end();
                                                }
                                                uniEmitter.complete(null);
                                            } catch (Throwable t) {
                                                log.error("Unable to complete post authentication", t);
                                                uniEmitter.fail(t);
                                            }
                                        }
                                    }, new Consumer() {
                                        @Override
                                        public void accept(Throwable throwable) {
                                            uniEmitter.fail(throwable);
                                        }
                                    });
                        } catch (Throwable t) {
                            uniEmitter.fail(t);
                        }
                    }
                });
                exchange.request().resume();
            }
        });
    }

    protected void handleRedirectBack(final RoutingContext exchange) {
        Cookie redirect = exchange.request().getCookie(locationCookie);
        String location;
        if (redirect != null) {
            verifyRedirectBackLocation(exchange.request().absoluteURI(), redirect.getValue());
            redirect.setSecure(exchange.request().isSSL());
            redirect.setSameSite(cookieSameSite);
            location = redirect.getValue();
            exchange.response().addCookie(redirect.setMaxAge(0));
        } else {
            if (landingPage == null) {
                // we know this won't happen with default implementation as we only call handleRedirectBack
                // when landingPage is not null, however we can't control inheritors
                throw new IllegalStateException(
                        "Landing page is no set, please make sure 'quarkus.http.auth.form.landing-page' is configured properly.");
            }
            location = exchange.request().scheme() + "://" + exchange.request().authority() + landingPage;
        }
        exchange.response().setStatusCode(302);
        exchange.response().headers().add(HttpHeaderNames.LOCATION, location);
        exchange.response().end();
    }

    protected void verifyRedirectBackLocation(String requestURIString, String redirectUriString) {
        URI requestUri = URI.create(requestURIString);
        URI redirectUri = URI.create(redirectUriString);
        if (!requestUri.getAuthority().equals(redirectUri.getAuthority())
                || !requestUri.getScheme().equals(redirectUri.getScheme())) {
            log.errorf("Location cookie value %s does not match the current request URI %s's scheme, host or port",
                    redirectUriString,
                    requestURIString);
            throw new AuthenticationCompletionException();
        }
    }

    protected void storeInitialLocation(final RoutingContext exchange) {
        exchange.response().addCookie(Cookie.cookie(locationCookie, exchange.request().absoluteURI())
                .setPath(cookiePath).setSameSite(cookieSameSite).setSecure(exchange.request().isSSL()));
    }

    protected void servePage(final RoutingContext exchange, final String location) {
        sendRedirect(exchange, location);
    }

    static void sendRedirect(final RoutingContext exchange, final String location) {
        String loc = exchange.request().scheme() + "://" + exchange.request().authority() + location;
        exchange.response().headers().add(HttpHeaderNames.LOCATION, loc);
        exchange.response().setStatusCode(302);
        exchange.response().end();
    }

    static Uni getRedirect(final RoutingContext exchange, final String location) {
        String loc = exchange.request().scheme() + "://" + exchange.request().authority() + location;
        return Uni.createFrom().item(new ChallengeData(302, "Location", loc));
    }

    @Override
    public Uni authenticate(RoutingContext context,
            IdentityProviderManager identityProviderManager) {

        if (context.normalizedPath().endsWith(postLocation) && context.request().method().equals(HttpMethod.POST)) {
            //we always re-auth if it is a post to the auth URL
            context.put(HttpAuthenticationMechanism.class.getName(), this);
            return runFormAuth(context, identityProviderManager);
        } else {
            PersistentLoginManager.RestoreResult result = loginManager.restore(context);
            if (result != null) {
                context.put(HttpAuthenticationMechanism.class.getName(), this);
                Uni ret = identityProviderManager
                        .authenticate(HttpSecurityUtils
                                .setRoutingContextAttribute(new TrustedAuthenticationRequest(result.getPrincipal()), context));
                return ret.onItem().invoke(new Consumer() {
                    @Override
                    public void accept(SecurityIdentity securityIdentity) {
                        loginManager.save(securityIdentity, context, result, context.request().isSSL());
                    }
                });
            }
            return Uni.createFrom().optional(Optional.empty());
        }
    }

    @Override
    public Uni getChallenge(RoutingContext context) {
        if (context.normalizedPath().endsWith(postLocation) && context.request().method().equals(HttpMethod.POST)) {
            if (redirectToErrorPage) {
                log.debugf("Serving form auth error page %s for %s", errorPage, context);
                // This method would no longer be called if authentication had already occurred.
                return getRedirect(context, errorPage);
            }
        } else {
            if (redirectToLoginPage) {
                log.debugf("Serving login form %s for %s", loginPage, context);
                // we need to store the URL
                storeInitialLocation(context);
                return getRedirect(context, loginPage);
            }
        }

        // redirect is disabled
        return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), null, null));
    }

    @Override
    public Set> getCredentialTypes() {
        return new HashSet<>(Arrays.asList(UsernamePasswordAuthenticationRequest.class, TrustedAuthenticationRequest.class));
    }

    @Override
    public Uni getCredentialTransport(RoutingContext context) {
        return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM));
    }

    private static String startWithSlash(String page) {
        if (page == null) {
            return null;
        }
        return page.startsWith("/") ? page : "/" + page;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy