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

io.gravitee.am.identityprovider.http.authentication.HttpAuthenticationProvider Maven / Gradle / Ivy

There is a newer version: 4.6.0-alpha.2
Show newest version
/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * 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
 *
 *         http://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.gravitee.am.identityprovider.http.authentication;

import io.gravitee.am.common.exception.authentication.AuthenticationException;
import io.gravitee.am.common.exception.authentication.InternalAuthenticationServiceException;
import io.gravitee.am.common.oidc.StandardClaims;
import io.gravitee.am.identityprovider.api.*;
import io.gravitee.am.identityprovider.http.HttpIdentityProviderResponse;
import io.gravitee.am.identityprovider.http.authentication.spring.HttpAuthenticationProviderConfiguration;
import io.gravitee.am.identityprovider.http.configuration.HttpAuthResourcePathsConfiguration;
import io.gravitee.am.identityprovider.http.configuration.HttpIdentityProviderConfiguration;
import io.gravitee.am.identityprovider.http.configuration.HttpResourceConfiguration;
import io.gravitee.am.identityprovider.http.configuration.HttpResponseErrorCondition;
import io.gravitee.am.identityprovider.http.utils.SanitizeUtils;
import io.gravitee.am.service.authentication.crypto.password.PasswordEncoder;
import io.gravitee.am.service.exception.AbstractManagementException;
import io.gravitee.am.service.exception.TechnicalManagementException;
import io.gravitee.common.http.HttpHeader;
import io.gravitee.common.http.HttpHeaders;
import io.gravitee.common.http.MediaType;
import io.gravitee.el.TemplateEngine;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.rxjava3.core.MultiMap;
import io.vertx.rxjava3.core.buffer.Buffer;
import io.vertx.rxjava3.ext.web.client.HttpRequest;
import io.vertx.rxjava3.ext.web.client.HttpResponse;
import io.vertx.rxjava3.ext.web.client.WebClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Import;

import java.lang.reflect.Constructor;
import java.util.*;

/**
 * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
 * @author GraviteeSource Team
 */
@Import(HttpAuthenticationProviderConfiguration.class)
public class HttpAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpAuthenticationProvider.class);
    private static final String PRINCIPAL_CONTEXT_KEY = "principal";
    private static final String CREDENTIALS_CONTEXT_KEY = "credentials";
    private static final String AUTHENTICATION_RESPONSE_CONTEXT_KEY = "authenticationResponse";
    private static final String USER_CONTEXT_KEY = "user";

    @Autowired
    @Qualifier("idpHttpAuthWebClient")
    private WebClient client;

    @Autowired
    private HttpIdentityProviderConfiguration configuration;

    @Autowired
    private IdentityProviderMapper mapper;

    @Autowired
    private IdentityProviderRoleMapper roleMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Maybe loadUserByUsername(Authentication authentication) {
        try {
            // prepare request
            final HttpResourceConfiguration resourceConfiguration = configuration.getAuthenticationResource();
            final HttpMethod authenticationHttpMethod = HttpMethod.valueOf(resourceConfiguration.getHttpMethod().toString());
            final List authenticationHttpHeaders = resourceConfiguration.getHttpHeaders();
            final String authenticationBody = resourceConfiguration.getHttpBody();

            final Object principal = authentication.getPrincipal();
            final String encodedCredentials = passwordEncoder.encode((String) authentication.getCredentials());
            final Object credentials = SanitizeUtils.sanitize(encodedCredentials, authenticationBody, authenticationHttpHeaders);

            // prepare context
            TemplateEngine templateEngine = authentication.getContext().getTemplateEngine();
            templateEngine.getTemplateContext().setVariable(PRINCIPAL_CONTEXT_KEY, principal);
            templateEngine.getTemplateContext().setVariable(CREDENTIALS_CONTEXT_KEY, credentials);

            // process request
            final String authenticationURI = templateEngine.getValue(resourceConfiguration.getBaseURL(), String.class);
            final Single> requestHandler = processRequest(templateEngine, authenticationURI, authenticationHttpMethod, authenticationHttpHeaders, authenticationBody);

            return requestHandler
                    .toMaybe()
                    .map(httpResponse -> {
                        final List errorConditions = resourceConfiguration.getHttpResponseErrorConditions();
                        Map userAttributes = processResponse(templateEngine, errorConditions, httpResponse);
                        return createUser(authentication.getContext(), userAttributes);
                    })
                    .onErrorResumeNext(ex -> {
                        if (ex instanceof AuthenticationException) {
                            return Maybe.error(ex);
                        }
                        LOGGER.error("An error has occurred while calling the remote HTTP identity provider {}", ex);
                        return Maybe.error(new InternalAuthenticationServiceException("An error has occurred while calling the remote HTTP identity provider", ex));
                    });
        } catch (Exception ex) {
            LOGGER.error("An error has occurred while authenticating the user {}", ex);
            return Maybe.error(new InternalAuthenticationServiceException("An error has occurred while authenticating the user", ex));
        }
    }

    @Override
    public Maybe loadPreAuthenticatedUser(Authentication authentication) {
        return loadByUsername0(authentication.getContext(), new DefaultUser((io.gravitee.am.model.User) authentication.getPrincipal()));
    }

    @Override
    public Maybe loadUserByUsername(String username) {
        return loadByUsername0(new SimpleAuthenticationContext(), new DefaultUser(username));
    }

    private Maybe loadByUsername0(AuthenticationContext authenticationContext, User user) {
        // prepare request
        final HttpAuthResourcePathsConfiguration authResourceConfiguration = configuration.getAuthenticationResource().getPaths();
        if (authResourceConfiguration == null) {
            return Maybe.empty();
        }
        if (authResourceConfiguration.getLoadPreAuthUserResource() == null) {
            return Maybe.empty();
        }

        final HttpResourceConfiguration readResourceConfiguration = authResourceConfiguration.getLoadPreAuthUserResource();

        if (readResourceConfiguration.getBaseURL() == null) {
            LOGGER.warn("Missing pre-authenticated user resource base URL");
            return Maybe.empty();
        }

        if (readResourceConfiguration.getHttpMethod() == null) {
            LOGGER.warn("Missing pre-authenticated user resource HTTP method");
            return Maybe.empty();
        }

        try {
            // prepare context
            TemplateEngine templateEngine = authenticationContext.getTemplateEngine();
            templateEngine.getTemplateContext().setVariable(USER_CONTEXT_KEY, user);

            // prepare request
            final String readUserURI = readResourceConfiguration.getBaseURL();
            final HttpMethod readUserHttpMethod = HttpMethod.valueOf(readResourceConfiguration.getHttpMethod().toString());
            final List readUserHttpHeaders = readResourceConfiguration.getHttpHeaders();
            final String readUserBody = readResourceConfiguration.getHttpBody();
            final Single> requestHandler = processRequest(templateEngine, readUserURI, readUserHttpMethod, readUserHttpHeaders, readUserBody);

            return requestHandler
                    .toMaybe()
                    .map(httpResponse -> {
                        final List errorConditions = readResourceConfiguration.getHttpResponseErrorConditions();
                        Map userAttributes = processResponse(templateEngine, errorConditions, httpResponse);
                        return createUser(authenticationContext, userAttributes);
                    })
                    .onErrorResumeNext(ex -> {
                        if (ex instanceof AbstractManagementException) {
                            return Maybe.error(ex);
                        }
                        LOGGER.error("An error has occurred when loading pre-authenticated user {} from the remote HTTP identity provider", user.getUsername() != null ? user.getUsername() : user.getEmail(), ex);
                        return Maybe.error(new TechnicalManagementException("An error has occurred when loading pre-authenticated user from the remote HTTP identity provider", ex));
                    });
        } catch (Exception ex) {
            LOGGER.error("An error has occurred when loading pre-authenticated user {}", user.getUsername() != null ? user.getUsername() : user.getEmail(), ex);
            return Maybe.error(new TechnicalManagementException("An error has occurred when when loading pre-authenticated user", ex));
        }
    }

    private Single> processRequest(TemplateEngine templateEngine,
                                                        String httpURI,
                                                        HttpMethod httpMethod,
                                                        List httpHeaders,
                                                        String httpBody) {
        // prepare request
        final String evaluatedHttpURI = templateEngine.getValue(httpURI, String.class);
        final HttpRequest httpRequest = client.requestAbs(httpMethod, evaluatedHttpURI);

        // set headers
        if (httpHeaders != null) {
            httpHeaders.forEach(header -> {
                String extValue = templateEngine.getValue(header.getValue(), String.class);
                httpRequest.putHeader(header.getName(), extValue);
            });
        }

        // set body
        Single> responseHandler;
        if (httpBody != null && !httpBody.isEmpty()) {
            String bodyRequest = templateEngine.getValue(httpBody, String.class);
            if (!httpRequest.headers().contains(HttpHeaders.CONTENT_TYPE)) {
                httpRequest.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bodyRequest.length()));
                responseHandler = httpRequest.rxSendBuffer(Buffer.buffer(bodyRequest));
            } else {
                String contentTypeHeader = httpRequest.headers().get(HttpHeaders.CONTENT_TYPE);
                switch (contentTypeHeader) {
                    case(MediaType.APPLICATION_JSON):
                        responseHandler = httpRequest.rxSendJsonObject(new JsonObject(bodyRequest));
                        break;
                    case(MediaType.APPLICATION_FORM_URLENCODED):
                        Map queryParameters = format(bodyRequest);
                        MultiMap multiMap = MultiMap.caseInsensitiveMultiMap();
                        multiMap.setAll(queryParameters);
                        responseHandler = httpRequest.rxSendForm(multiMap);
                        break;
                    default:
                        httpRequest.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bodyRequest.length()));
                        responseHandler = httpRequest.rxSendBuffer(Buffer.buffer(bodyRequest));
                }
            }
        } else {
            responseHandler = httpRequest.rxSend();
        }
        return responseHandler;
    }

    private Map processResponse(TemplateEngine templateEngine, List errorConditions, HttpResponse httpResponse) throws Exception {
        String responseBody =  httpResponse.bodyAsString();
        templateEngine.getTemplateContext().setVariable(AUTHENTICATION_RESPONSE_CONTEXT_KEY, new HttpIdentityProviderResponse(httpResponse, responseBody));

        // process response
        Exception lastException = null;
        if (errorConditions != null) {
            Iterator iter = errorConditions.iterator();
            while (iter.hasNext() && lastException == null) {
                HttpResponseErrorCondition errorCondition = iter.next();
                if (templateEngine.getValue(errorCondition.getValue(), Boolean.class)) {
                    Class clazz = (Class) Class.forName(errorCondition.getException());
                    if (errorCondition.getMessage() != null) {
                        String errorMessage = templateEngine.getValue(errorCondition.getMessage(), String.class);
                        Constructor constructor = clazz.getConstructor(String.class);
                        lastException = clazz.cast(constructor.newInstance(new Object[]{errorMessage}));
                    } else {
                        lastException = clazz.newInstance();
                    }
                }
            }
        }

        // if remote API call failed, throw exception
        if (lastException != null) {
            throw lastException;
        }
        if (responseBody == null) {
            throw new InternalAuthenticationServiceException("Unable to find user information");
        }
        return responseBody.startsWith("[") ?
                new JsonArray(responseBody).getJsonObject(0).getMap() : new JsonObject(responseBody).getMap();
    }

    private User createUser(AuthenticationContext authContext, Map attributes) {
        // apply user mapping
        Map mappedAttributes = applyUserMapping(authContext, attributes);

        // sub claim is required
        if (mappedAttributes.isEmpty() || mappedAttributes.get(StandardClaims.SUB) == null) {
            throw new InternalAuthenticationServiceException("The 'sub' claim for the user is required");
        }

        // apply role mapping
        List roles = applyRoleMapping(authContext, attributes);

        // create the user
        String username = mappedAttributes.getOrDefault(StandardClaims.PREFERRED_USERNAME, mappedAttributes.get(StandardClaims.SUB)).toString();
        DefaultUser user = new DefaultUser(username);
        user.setId(mappedAttributes.get(StandardClaims.SUB).toString());
        // set additional information
        Map additionalInformation = new HashMap<>();
        additionalInformation.putAll(mappedAttributes);
        // update username if user mapping has been changed
        if (additionalInformation.containsKey(StandardClaims.PREFERRED_USERNAME)) {
            user.setUsername(additionalInformation.get(StandardClaims.PREFERRED_USERNAME).toString());
        }
        user.setAdditionalInformation(additionalInformation);
        // set user roles
        user.setRoles(roles);
        return user;
    }

    private Map applyUserMapping(AuthenticationContext authContext, Map attributes) {
        if (!mappingEnabled()) {
            return attributes;
        }
        return this.mapper.apply(authContext, attributes);
    }

    private List applyRoleMapping(AuthenticationContext authContext, Map attributes) {
        if (!roleMappingEnabled()) {
            return Collections.emptyList();
        }
        return this.roleMapper.apply(authContext, attributes);
    }

    private boolean mappingEnabled() {
        return this.mapper != null;
    }

    private boolean roleMappingEnabled() {
        return this.roleMapper != null;
    }

    private static Map format(String query) {
        Map queryPairs = new LinkedHashMap();
        String[] pairs = query.split("&");
        for (String pair : pairs) {
            int idx = pair.indexOf("=");
            queryPairs.put(pair.substring(0, idx), pair.substring(idx + 1));
        }
        return queryPairs;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy