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

io.micronaut.security.authentication.Authenticator Maven / Gradle / Ivy

There is a newer version: 4.11.1
Show newest version
/*
 * Copyright 2017-2023 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.security.authentication;

import io.micronaut.context.BeanContext;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.order.OrderUtil;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.security.authentication.provider.AuthenticationProvider;
import io.micronaut.security.authentication.provider.ExecutorAuthenticationProvider;
import io.micronaut.security.authentication.provider.ReactiveAuthenticationProvider;
import io.micronaut.security.authentication.provider.ReactiveAuthenticationProviderAdapter;
import io.micronaut.security.config.AuthenticationStrategy;
import io.micronaut.security.config.SecurityConfiguration;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * An Authenticator operates on several {@link ReactiveAuthenticationProvider} instances returning the first
 * authenticated {@link AuthenticationResponse}.
 *
 * @author Sergio del Amo
 * @author Graeme Rocher
 * @since 1.0
 * @param  Request Context Type
 */
@Singleton
public class Authenticator {
    private static final Logger LOG = LoggerFactory.getLogger(Authenticator.class);

    /**
     *
     * @deprecated Unused. To be removed in the next major version.
     */
    @Deprecated(forRemoval = true, since = "4.5.0")
    protected final Collection> authenticationProviders;

    private final List> reactiveAuthenticationProviders;
    private final BeanContext beanContext;

    private final List> imperativeAuthenticationProviders;
    private final SecurityConfiguration securityConfiguration;

    private final Map executeNameToScheduler = new ConcurrentHashMap<>();


    /**
     * @param beanContext Bean Context
     * @param reactiveAuthenticationProviders A list of available Reactive authentication providers
     * @param authenticationProviders A list of available imperative authentication providers
     * @param deprecatedAuthenticationProviders A list of available deprecated authentication providers
     * @param securityConfiguration The security configuration
     */
    @Inject
    public Authenticator(BeanContext beanContext,
                         List> reactiveAuthenticationProviders,
                         List> authenticationProviders,
                         List> deprecatedAuthenticationProviders,
                         SecurityConfiguration securityConfiguration) {
        this.beanContext = beanContext;
        this.reactiveAuthenticationProviders = reactiveAuthenticationProviders;
        for (io.micronaut.security.authentication.AuthenticationProvider authenticationProvider : deprecatedAuthenticationProviders) {
            reactiveAuthenticationProviders.add((new ReactiveAuthenticationProviderAdapter<>(authenticationProvider)));
        }
        this.securityConfiguration = securityConfiguration;
        this.imperativeAuthenticationProviders = authenticationProviders;
        this.authenticationProviders = Collections.emptyList();
    }

    /**
     * @param beanContext Bean Context
     * @param reactiveAuthenticationProviders A list of available Reactive authentication providers
     * @param authenticationProviders A list of available imperative authentication providers
     * @param securityConfiguration The security configuration
     * @deprecated Use {@link Authenticator#Authenticator(BeanContext, List, List, List, SecurityConfiguration)} instead.
     */
    public Authenticator(BeanContext beanContext,
                         List> reactiveAuthenticationProviders,
                         List> authenticationProviders,
                         SecurityConfiguration securityConfiguration) {
        this.beanContext = beanContext;
        this.reactiveAuthenticationProviders = reactiveAuthenticationProviders;
        this.securityConfiguration = securityConfiguration;
        this.imperativeAuthenticationProviders = authenticationProviders;
        this.authenticationProviders = Collections.emptyList();
    }

    /**
     * @param deprecatedAuthenticationProviders A list of available authentication providers
     * @param securityConfiguration   The security configuration
     * @deprecated Use {@link Authenticator#Authenticator(BeanContext, List, List, SecurityConfiguration)} instead.
     */
    @Deprecated(forRemoval = true, since = "4.5.0")
    public Authenticator(Collection> deprecatedAuthenticationProviders,
                         SecurityConfiguration securityConfiguration) {
        this.beanContext = null;
        this.authenticationProviders = deprecatedAuthenticationProviders;
        reactiveAuthenticationProviders = new ArrayList<>();
        for (io.micronaut.security.authentication.AuthenticationProvider authenticationProvider : deprecatedAuthenticationProviders) {
            reactiveAuthenticationProviders.add((new ReactiveAuthenticationProviderAdapter<>(authenticationProvider)));
        }
        this.securityConfiguration = securityConfiguration;
        this.imperativeAuthenticationProviders = Collections.emptyList();
    }

    /**
     * Authenticates the user with the provided credentials.
     *
     * @param requestContext           The HTTP request
     * @param authenticationRequest Represents a request to authenticate.
     * @return A publisher that emits {@link AuthenticationResponse} objects
     */
    public Publisher authenticate(T requestContext, AuthenticationRequest authenticationRequest) {
        if (CollectionUtils.isEmpty(reactiveAuthenticationProviders) && CollectionUtils.isEmpty(imperativeAuthenticationProviders)) {
            return Mono.empty();
        }
        if (LOG.isDebugEnabled() && imperativeAuthenticationProviders != null) {
            LOG.debug(imperativeAuthenticationProviders.stream().map(AuthenticationProvider::getClass).map(Class::getName).collect(Collectors.joining()));
        }
        if (LOG.isDebugEnabled() && reactiveAuthenticationProviders != null) {
            LOG.debug(reactiveAuthenticationProviders.stream().map(ReactiveAuthenticationProvider::getClass).map(Class::getName).collect(Collectors.joining()));
        }
        if (CollectionUtils.isEmpty(reactiveAuthenticationProviders) && imperativeAuthenticationProviders != null && !anyImperativeAuthenticationProviderIsBlocking()) {
            return Mono.just(authenticate(requestContext, authenticationRequest, imperativeAuthenticationProviders, securityConfiguration));
        }
        return authenticate(requestContext, authenticationRequest, everyProviderSorted());
    }

    /**
     *
     * @return Whether any of the authentication provider is blocking
     */
    private boolean anyImperativeAuthenticationProviderIsBlocking() {
        return imperativeAuthenticationProviders.stream().anyMatch(this::isImperativeAuthenticationProviderIsBlocking);
    }

    /**
     * If {@link ExecutorAuthenticationProvider#getExecutorName()} equals `blocking` or `io` returns `true`.
     * @param authenticationProvider An authentication provider
     * @return Whether any of the authentication provider is blocking.
     */
    protected boolean isImperativeAuthenticationProviderIsBlocking(AuthenticationProvider authenticationProvider) {
        return authenticationProvider instanceof ExecutorAuthenticationProvider ap && (ap.getExecutorName().equals(TaskExecutors.BLOCKING) || ap.getExecutorName().equals(TaskExecutors.IO));
    }

    @NonNull
    private AuthenticationResponse authenticate(@NonNull T requestContext,
                                                @NonNull AuthenticationRequest authenticationRequest,
                                                @NonNull List> authenticationProviders,
                                                @Nullable SecurityConfiguration securityConfiguration) {
        if (securityConfiguration != null && securityConfiguration.getAuthenticationProviderStrategy() == AuthenticationStrategy.ALL) {
            return authenticateAll(requestContext, authenticationRequest, authenticationProviders);
        }
        List responses = new ArrayList<>();
        for (AuthenticationProvider provider : authenticationProviders) {
            AuthenticationResponse response = authenticationResponse(provider, requestContext, authenticationRequest);
            if (response.isAuthenticated()) {
                return response;
            }
            responses.add(response);
        }
        return responses.stream()
                        .findFirst()
                        .orElseGet(AuthenticationResponse::failure);
    }

    @NonNull
    private AuthenticationResponse authenticateAll(@NonNull T requestContext,
                                                   @NonNull AuthenticationRequest authenticationRequest,
                                                   @NonNull List> authenticationProviders) {
        List authenticationResponses = authenticationProviders.stream()
                        .map(provider -> authenticationResponse(provider, requestContext, authenticationRequest))
                        .toList();
        if (CollectionUtils.isEmpty(authenticationResponses)) {
            return AuthenticationResponse.failure();
        }
        return authenticationResponses.stream().allMatch(AuthenticationResponse::isAuthenticated)
                ? authenticationResponses.get(0)
                : AuthenticationResponse.failure();
    }

    private List> everyProviderSorted() {
        List> providers = new ArrayList<>(reactiveAuthenticationProviders);
        if (beanContext != null) {
            providers.addAll(imperativeAuthenticationProviders.stream()
                    .map(imperativeAuthenticationProvider -> {
                        if (imperativeAuthenticationProvider instanceof ExecutorAuthenticationProvider ap) {
                            return new AuthenticationProviderAdapter<>(imperativeAuthenticationProvider, executeNameToScheduler.computeIfAbsent(ap.getExecutorName(), s ->
                                    beanContext.findBean(ExecutorService.class, Qualifiers.byName(ap.getExecutorName()))
                                            .map(Schedulers::fromExecutorService)
                                            .orElse(null)));
                        } else {
                            return new AuthenticationProviderAdapter<>(imperativeAuthenticationProvider);
                        }
                    }).toList());
        }
        OrderUtil.sort(providers);
        return providers;
    }

    private Publisher authenticate(T request,
                                                           AuthenticationRequest authenticationRequest,
                                                           List> providers) {
        if (providers == null) {
            return Flux.empty();
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug(providers.stream().map(ReactiveAuthenticationProvider::getClass).map(Class::getName).collect(Collectors.joining()));
        }
        Flux[] emptyArr = new Flux[0];
        if (securityConfiguration != null && securityConfiguration.getAuthenticationProviderStrategy() == AuthenticationStrategy.ALL) {

            return Flux.mergeDelayError(1,
                            providers.stream()
                            .map(provider ->
                                Flux.from(provider.authenticate(request, authenticationRequest))
                                        .switchMap(rsp -> Authenticator.handleResponse((AuthenticationResponse) rsp))
                                        .switchIfEmpty(Flux.error(() -> new AuthenticationException("Provider did not respond. Authentication rejected")))
                            )
                            .toList()
                    .toArray(emptyArr))
                    .last()
                    .onErrorResume(t -> Mono.just(authenticationResponseForThrowable(t)))
                    .flux();
        } else {
            AtomicReference lastError = new AtomicReference<>();
            Flux authentication = Flux.mergeDelayError(1,  providers.stream()
                    .map(auth -> Flux.from(auth.authenticate(request, authenticationRequest)))
                    .map(sequence -> sequence.switchMap(rsp -> Authenticator.handleResponse((AuthenticationResponse) rsp))
                            .onErrorResume((Function) t -> {
                                lastError.set(t);
                                return Flux.empty();
                            })
                            ).toList()
                    .toArray(emptyArr));

            return authentication.take(1)
                    .switchIfEmpty(Flux.create(emitter -> {
                Throwable error = lastError.get();
                if (error != null) {
                    if (error instanceof AuthenticationException) {
                        AuthenticationResponse response = ((AuthenticationException) error).getResponse();
                        if (response != null) {
                            emitter.next(response);
                            emitter.complete();
                        } else {
                            emitter.error(error);
                        }
                    } else {
                        emitter.error(error);
                    }
                } else {
                    emitter.complete();
                }
            }, FluxSink.OverflowStrategy.ERROR));
        }
    }

    private static Mono handleResponse(AuthenticationResponse response) {
        if (response.isAuthenticated()) {
            return Mono.just(response);
        } else {
            return Mono.error(new AuthenticationException(response));
        }
    }

    @NonNull
    private AuthenticationResponse authenticationResponse(@NonNull AuthenticationProvider provider,
                                                          @NonNull T requestContext,
                                                          @NonNull AuthenticationRequest authenticationRequest) {
        try {
            return provider.authenticate(requestContext, authenticationRequest);
        } catch (Exception t) {
            return authenticationResponseForThrowable(t);
        }
    }

    @NonNull
    private static AuthenticationResponse authenticationResponseForThrowable(Throwable t) {
        if (Exceptions.isMultiple(t)) {
            List exceptions = Exceptions.unwrapMultiple(t);
            return new AuthenticationFailed(exceptions.get(exceptions.size() - 1).getMessage());
        }
        return new AuthenticationFailed(t.getMessage());
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy