eu.eidas.node.auth.connector.AUCONNECTORSAML.java Source code

Java tutorial

Introduction

Here is the source code for eu.eidas.node.auth.connector.AUCONNECTORSAML.java

Source

/*
 * Copyright (c) 2015 by European Commission
 *
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 * http://www.osor.eu/eupl/european-union-public-licence-eupl-v.1.1
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and
 * limitations under the Licence.
 *
 * This product combines work with different licenses. See the "NOTICE" text
 * file for details on the various modules and licenses.
 * The "NOTICE" text file is part of the distribution. Any derivative works
 * that you distribute must include a readable copy of the "NOTICE" text file.
 *
 */

package eu.eidas.node.auth.connector;

import java.util.Locale;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import com.google.common.collect.ImmutableSet;

import org.apache.commons.lang.StringUtils;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;

import eu.eidas.auth.commons.DateUtil;
import eu.eidas.auth.commons.EIDASStatusCode;
import eu.eidas.auth.commons.EIDASUtil;
import eu.eidas.auth.commons.EIDASValues;
import eu.eidas.auth.commons.EidasDigestUtil;
import eu.eidas.auth.commons.EidasErrorKey;
import eu.eidas.auth.commons.EidasErrors;
import eu.eidas.auth.commons.EidasParameterKeys;
import eu.eidas.auth.commons.EidasStringUtil;
import eu.eidas.auth.commons.IEIDASLogger;
import eu.eidas.auth.commons.RequestState;
import eu.eidas.auth.commons.WebRequest;
import eu.eidas.auth.commons.attribute.ImmutableAttributeMap;
import eu.eidas.auth.commons.attribute.impl.StringAttributeValue;
import eu.eidas.auth.commons.exceptions.InternalErrorEIDASException;
import eu.eidas.auth.commons.exceptions.InvalidParameterEIDASException;
import eu.eidas.auth.commons.exceptions.InvalidSessionEIDASException;
import eu.eidas.auth.commons.exceptions.SecurityEIDASException;
import eu.eidas.auth.commons.light.ILightRequest;
import eu.eidas.auth.commons.protocol.IAuthenticationRequest;
import eu.eidas.auth.commons.protocol.IAuthenticationResponse;
import eu.eidas.auth.commons.protocol.IRequestMessage;
import eu.eidas.auth.commons.protocol.IResponseMessage;
import eu.eidas.auth.commons.protocol.eidas.IEidasAuthenticationRequest;
import eu.eidas.auth.commons.protocol.eidas.LevelOfAssurance;
import eu.eidas.auth.commons.protocol.eidas.impl.EidasAuthenticationRequest;
import eu.eidas.auth.commons.protocol.impl.AuthenticationResponse;
import eu.eidas.auth.commons.protocol.impl.EidasSamlBinding;
import eu.eidas.auth.commons.protocol.impl.SamlBindingUri;
import eu.eidas.auth.commons.protocol.stork.IStorkAuthenticationRequest;
import eu.eidas.auth.commons.tx.AuthenticationExchange;
import eu.eidas.auth.commons.tx.CorrelationMap;
import eu.eidas.auth.commons.tx.StoredAuthenticationRequest;
import eu.eidas.auth.commons.tx.StoredLightRequest;
import eu.eidas.auth.commons.validation.NormalParameterValidator;
import eu.eidas.auth.engine.Correlated;
import eu.eidas.auth.engine.ProtocolEngineFactory;
import eu.eidas.auth.engine.ProtocolEngineI;
import eu.eidas.auth.engine.core.eidas.spec.EidasSpec;
import eu.eidas.auth.engine.metadata.MetadataFetcherI;
import eu.eidas.auth.engine.metadata.MetadataSignerI;
import eu.eidas.auth.engine.metadata.MetadataUtil;
import eu.eidas.auth.engine.xml.opensaml.SAMLEngineUtils;
import eu.eidas.engine.exceptions.EIDASSAMLEngineException;
import eu.eidas.node.logging.LoggingMarkerMDC;
import eu.eidas.node.utils.EidasNodeErrorUtil;
import eu.eidas.node.utils.EidasNodeValidationUtil;
import eu.eidas.node.utils.PropertiesUtil;
import eu.eidas.node.utils.SessionHolder;

/**
 * This class is used by {@link AUCONNECTOR} to get, process and generate SAML Tokens.
 *
 * @see ICONNECTORSAMLService
 */
public final class AUCONNECTORSAML implements ICONNECTORSAMLService {

    /**
     * Logger object.
     */
    private static final Logger LOG = LoggerFactory.getLogger(AUCONNECTORSAML.class);

    /**
     * Request logging.
     */
    private static final Logger LOGGER_COM_REQ = LoggerFactory.getLogger(
            EIDASValues.EIDAS_PACKAGE_REQUEST_LOGGER_VALUE.toString() + "." + AUCONNECTOR.class.getSimpleName());

    /**
     * Response logging.
     */
    private static final Logger LOGGER_COM_RESP = LoggerFactory.getLogger(
            EIDASValues.EIDAS_PACKAGE_RESPONSE_LOGGER_VALUE.toString() + "." + AUCONNECTOR.class.getSimpleName());

    /**
     * Logger bean.
     */
    private IEIDASLogger loggerBean;

    /**
     * SAML instance to communicate with SP.
     */
    private String samlSpInstance;

    /**
     * SAML instance to communicate with ServiceProxy.
     */
    private String samlServiceInstance;

    /**
     * Connector's processAuthenticationResponse class.
     */
    private AUCONNECTORUtil connectorUtil;

    /**
     * metadata url to be put in requests generated by the Connector module.
     */
    private String metadataUrl;

    /**
     * metadata url to be put in responses generated by the Connector module.
     */
    private String metadataResponderUrl;

    /**
     * Resource bundle to translate messages from ServiceProxy/VIdP.
     */
    private MessageSource messageSource;

    private boolean checkCitizenCertificateServiceCertificate;

    private MetadataFetcherI metadataFetcher;

    private ProtocolEngineFactory nodeProtocolEngineFactory;

    public void setCheckCitizenCertificateServiceCertificate(boolean checkCitizenCertificateServiceCertificate) {
        this.checkCitizenCertificateServiceCertificate = checkCitizenCertificateServiceCertificate;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] generateErrorAuthenticationResponse(HttpServletRequest httpRequest, String destination,
            String statusCode, String subCode, String message) {
        EidasAuthenticationRequest.Builder request = new EidasAuthenticationRequest.Builder();
        request.id(EidasNodeErrorUtil.getInResponseTo(httpRequest));
        request.issuer(EidasNodeErrorUtil.getIssuer(httpRequest));
        request.destination(destination);
        request.citizenCountryCode(EidasNodeErrorUtil.getCitizenCountryCode(httpRequest));
        request.assertionConsumerServiceURL(destination);
        IAuthenticationRequest dummyRequest = request.build();

        return generateErrorAuthenticationResponse(dummyRequest, httpRequest.getRemoteAddr(), statusCode, subCode,
                message);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] generateErrorAuthenticationResponse(IAuthenticationRequest request, String ipUserAddress,
            String statusCode, String subCode, String message) {
        AuthenticationResponse.Builder samlResponseFail = new AuthenticationResponse.Builder();
        samlResponseFail.statusCode(statusCode);
        samlResponseFail.subStatusCode(subCode);
        samlResponseFail.statusMessage(message);
        samlResponseFail.issuer(getConnectorResponderMetadataUrl());
        samlResponseFail.inResponseTo(request.getId());
        samlResponseFail.id(SAMLEngineUtils.generateNCName());
        IAuthenticationResponse response = samlResponseFail.build();

        return generateErrorAuthenticationResponse(request, ipUserAddress, response, message);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] generateErrorAuthenticationResponse(IAuthenticationRequest request, String ipUserAddress,
            IAuthenticationResponse response, String message) {
        try {
            ProtocolEngineI engine = getSamlEngine(samlSpInstance);
            // Generate SAMLResponse Fail.
            String inResponseTo = request.getId();

            AuthenticationResponse.Builder samlResponseFail = new AuthenticationResponse.Builder();
            samlResponseFail.id(response.getId());
            samlResponseFail.statusCode(response.getSubStatusCode());
            samlResponseFail.subStatusCode(response.getSubStatusCode());
            samlResponseFail.statusMessage(message);
            samlResponseFail.issuer(getConnectorResponderMetadataUrl());
            samlResponseFail.inResponseTo(inResponseTo);

            IResponseMessage responseMessage = engine.generateResponseErrorMessage(request,
                    samlResponseFail.build(), ipUserAddress);

            prepareRespLoggerBean(EIDASValues.SP_RESPONSE.toString(), responseMessage.getResponse(), inResponseTo);
            saveLog(AUCONNECTORSAML.LOGGER_COM_RESP);
            LOG.info(LoggingMarkerMDC.SAML_EXCHANGE,
                    "Connector - Generating ERROR SAML Response to request with ID {}, error is {} {}",
                    inResponseTo, response.getSubStatusCode(), message);

            return responseMessage.getMessageBytes();
        } catch (EIDASSAMLEngineException e) {
            LOG.info("BUSINESS EXCEPTION : Error generating SAMLToken", e);
            EidasNodeErrorUtil.processSAMLEngineException(e, LOG,
                    getConnectorRedirectError(e, EidasErrorKey.SPROVIDER_SELECTOR_ERROR_CREATE_SAML));
            throw new InternalErrorEIDASException(
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_ERROR_CREATE_SAML.errorCode()),
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_ERROR_CREATE_SAML.errorMessage()), e);
        }
    }

    private EidasErrorKey getConnectorRedirectError(EIDASSAMLEngineException exc, EidasErrorKey defaultError) {
        EidasErrorKey redirectError = defaultError;
        EidasErrorKey actualError = EidasErrorKey.fromCode(exc.getErrorCode());
        if (actualError != null && actualError.isShowToUser()) {
            redirectError = actualError;
        }
        return redirectError;
    }

    /**
     * {@inheritDoc}
     */
    public byte[] extractResponseSAMLToken(WebRequest webRequest) {

        String strSamlToken;
        strSamlToken = webRequest.getEncodedLastParameterValue(EidasParameterKeys.SAML_RESPONSE);

        NormalParameterValidator.paramName(EidasParameterKeys.SAML_RESPONSE).paramValue(strSamlToken)
                .eidasError(EidasErrorKey.valueOf(EidasErrorKey.COLLEAGUE_RESP_INVALID_SAML.name())).validate();

        if (StringUtils.isBlank(strSamlToken)) {
            return null;
        }

        return EidasStringUtil.decodeBytesFromBase64(strSamlToken);
    }

    /*    @Nonnull
        @Override
        public ISpRequestResult processSpRequest(@Nonnull ILightRequest fromSpLightRequest)
        throws EIDASSAMLEngineException {
    return null;
        }*/

    /**
     * {@inheritDoc}
     */
    @Override
    public IAuthenticationRequest processSpRequest(@Nonnull ILightRequest lightRequest, WebRequest webRequest) {
        try {
            String serviceCode = getCountryCode(lightRequest, webRequest);

            LOG.debug("Requested country: " + serviceCode);

            String serviceMetadataURL = getConnectorUtil().loadConfigServiceMetadataURL(serviceCode);

            ProtocolEngineI engine = getSamlEngine(samlSpInstance);

            String serviceUrl = engine.getProtocolProcessor().getServiceUrl(serviceMetadataURL,
                    SamlBindingUri.SAML2_POST);

            if (StringUtils.isBlank(serviceUrl)) {
                // fallback to the locale configuration
                serviceUrl = connectorUtil.loadConfigServiceURL(serviceCode);
            }

            LOG.debug("Citizen Country URL " + serviceCode + " URL " + serviceUrl);
            NormalParameterValidator.paramName(EidasErrorKey.SERVICE_REDIRECT_URL.toString()).paramValue(serviceUrl)
                    .eidasError(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_COUNTRY).validate();

            LOG.info(LoggingMarkerMDC.SAML_EXCHANGE, "Connector - Processing LightRequest with ID {}",
                    lightRequest.getId());

            // Get Personal Attribute List and validate
            ImmutableAttributeMap requestedAttributes = lightRequest.getRequestedAttributes();

            NormalParameterValidator.paramName(EidasParameterKeys.ATTRIBUTE_LIST)
                    .paramValue(requestedAttributes.isEmpty() ? null : "dummy")
                    .eidasError(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_ATTR).validate();

            String levelOfAssurance = lightRequest.getLevelOfAssurance();
            RequestState requestState = webRequest.getRequestState();
            if (null != levelOfAssurance) {
                requestState.setLevelOfAssurance(levelOfAssurance);
            }

            // Get ProviderName and validate
            String providerName = lightRequest.getProviderName();
            NormalParameterValidator.paramName(EidasParameterKeys.PROVIDER_NAME_VALUE).paramValue(providerName)
                    .eidasError(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SP_PROVIDERNAME).validate();

            requestState.setProviderName(providerName);

            IAuthenticationRequest authnRequest = EidasAuthenticationRequest.builder().lightRequest(lightRequest)
                    .destination(serviceUrl).citizenCountryCode(serviceCode).build();

            validateRequestLoA(authnRequest, connectorUtil.loadConfigServiceMetadataURL(serviceCode));

            if (SessionHolder.getId() != null) {
                HttpSession session = SessionHolder.getId();
                session.setAttribute(EidasParameterKeys.SAML_IN_RESPONSE_TO.toString(), lightRequest.getId());
                session.setAttribute(EidasParameterKeys.ISSUER.toString(), lightRequest.getIssuer());
            }

            LOG.trace("Checking if SP is reliable");
            requestState.setInResponseTo(authnRequest.getId());
            requestState.setIssuer(authnRequest.getIssuer());
            requestState.setServiceUrl(authnRequest.getAssertionConsumerServiceURL());

            // Validate if SP has valid qaalevel and is trustworthy
            if (!connectorUtil.validateSP(webRequest)) {
                throw new InvalidParameterEIDASException(
                        EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SPQAAID.errorCode()),
                        EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SPQAAID.errorMessage()));
            }
            String metaDataUrl = webRequest.getEncodedLastParameterValue(EidasParameterKeys.SP_METADATA_URL);
            if (null != metaDataUrl && isIssuedBySelf(authnRequest)) {
                EidasAuthenticationRequest.Builder eIDASAuthnRequestBuilder = EidasAuthenticationRequest
                        .builder((IEidasAuthenticationRequest) authnRequest);
                eIDASAuthnRequestBuilder.issuer(metaDataUrl);
                authnRequest = eIDASAuthnRequestBuilder.build();
            }
            // Checking for antiReplay
            if (!connectorUtil.checkNotPresentInCache(authnRequest.getId(), authnRequest.getCitizenCountryCode())
                    .booleanValue()) {
                throw new SecurityEIDASException(
                        EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML.errorCode()),
                        EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML.errorMessage()));
            }

            return authnRequest;
        } catch (EIDASSAMLEngineException e) {
            // Special case for propagating the error in case of xxe
            EidasNodeErrorUtil.processSAMLEngineException(e, LOG,
                    getConnectorRedirectError(e, EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML));
            throw new InternalErrorEIDASException(
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML.errorCode()),
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML.errorMessage()), e);
        }
    }

    private void validateRequestLoA(IAuthenticationRequest authRequest, String idpUrl)
            throws EIDASSAMLEngineException {
        if (null == metadataFetcher) {
            return;
        }
        String colleagueLoA = MetadataUtil.getServiceLevelOfAssurance(metadataFetcher.getEntityDescriptor(idpUrl,
                (MetadataSignerI) getSamlEngine(samlServiceInstance).getSigner()));
        if (!StringUtils.isEmpty(colleagueLoA)
                && !EidasNodeValidationUtil.isRequestLoAValid(authRequest, colleagueLoA)) {
            throw new InternalErrorEIDASException(
                    EidasErrors.get(EidasErrorKey.SERVICE_PROVIDER_INVALID_LOA.errorCode()),
                    EidasErrors.get(EidasErrorKey.SERVICE_PROVIDER_INVALID_LOA.errorMessage()));
        }
    }

    private boolean isIssuedBySelf(IAuthenticationRequest authnRequest) {
        String connectorMetadataUrl = getConnectorMetadataUrl();
        return connectorMetadataUrl != null && connectorMetadataUrl.equalsIgnoreCase(authnRequest.getIssuer());
    }

    /**
     * Gets the Country Code.
     *
     * @param lightRequest The light authentication Request object.
     * @param webRequest the webRequest.
     * @return the country code value.
     */
    private static String getCountryCode(ILightRequest lightRequest, WebRequest webRequest) {
        // Country: Mandatory if the destination is a ProxyService.
        String serviceCode;
        if (lightRequest.getCitizenCountryCode() == null) {
            serviceCode = webRequest.getEncodedLastParameterValue(EidasParameterKeys.COUNTRY);

        } else {
            serviceCode = lightRequest.getCitizenCountryCode();
        }

        // Compatibility
        if (null != serviceCode && serviceCode.endsWith(EIDASValues.EIDAS_SERVICE_SUFFIX.toString())) {
            serviceCode = serviceCode.replace(EIDASValues.EIDAS_SERVICE_SUFFIX.toString(), StringUtils.EMPTY);
        }

        return serviceCode;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IRequestMessage generateServiceAuthnRequest(@Nonnull WebRequest webRequest,
            @Nonnull IAuthenticationRequest request) {

        String serviceCountryCode = getCountryCode(request, webRequest);

        //TODO check if tempAuthData creation could be avoided
        IRequestMessage tempAuthData = generateAuthenticationRequest(samlServiceInstance, request,
                serviceCountryCode);

        prepareReqLoggerBean(EIDASValues.EIDAS_CONNECTOR_REQUEST.toString(), tempAuthData.getMessageBytes(),
                tempAuthData.getRequest(), tempAuthData.getRequest().getId());

        saveLog(AUCONNECTORSAML.LOGGER_COM_REQ);
        LOG.trace("Logging communication");

        return tempAuthData;
    }

    private String extractErrorMessage(String defaultMsg, String errorCode) {
        String newErrorMessage = defaultMsg;
        try {
            newErrorMessage = messageSource.getMessage(errorCode, new Object[] { errorCode }, Locale.getDefault());
        } catch (NoSuchMessageException nsme) {
            LOG.warn("Cannot found the message with the id {} - {}", errorCode, nsme);
        }
        return newErrorMessage;
    }

    public AuthenticationExchange processProxyServiceResponse(@Nonnull WebRequest webRequest,
            @Nonnull CorrelationMap<StoredAuthenticationRequest> connectorRequestCorrelationMap,
            @Nonnull CorrelationMap<StoredLightRequest> specificSpRequestCorrelationMap)
            throws InternalErrorEIDASException {
        try {

            LOG.trace("Getting SAML Token");
            byte[] responseFromProxyService = this.extractResponseSAMLToken(webRequest);

            // validates SAML Token
            ProtocolEngineI engine = getSamlEngine(samlServiceInstance);

            Correlated proxyServiceSamlResponse = engine.unmarshallResponse(responseFromProxyService);

            String connectorRequestId = proxyServiceSamlResponse.getInResponseToId();

            if (StringUtils.isBlank(connectorRequestId)) {
                LOG.error("ERROR : SAML Response \"" + proxyServiceSamlResponse.getId() + "\" has no InResponseTo");
                throw new InvalidSessionEIDASException(EidasErrors.get(EidasErrorKey.AU_REQUEST_ID.errorCode()),
                        EidasErrors.get(EidasErrorKey.AU_REQUEST_ID.errorMessage()));
            }

            StoredAuthenticationRequest storedConnectorRequest = connectorRequestCorrelationMap
                    .get(connectorRequestId);
            StoredLightRequest storedServiceProviderRequest = specificSpRequestCorrelationMap
                    .get(connectorRequestId);
            if (null == storedConnectorRequest || null == storedServiceProviderRequest) {
                LOG.error("ERROR : SAML Response InResponseTo \"" + connectorRequestId
                        + "\" cannot be found in requestCorrelationMap");
                throw new InvalidSessionEIDASException(EidasErrors.get(EidasErrorKey.AU_REQUEST_ID.errorCode()),
                        EidasErrors.get(EidasErrorKey.AU_REQUEST_ID.errorMessage()));
            }

            String citizenIpAddress = storedConnectorRequest.getRemoteIpAddress();
            IAuthenticationRequest connectorAuthnRequest = storedConnectorRequest.getRequest();

            Long serviceSkew = connectorUtil
                    .loadConfigServiceTimeSkewInMillis(connectorAuthnRequest.getCitizenCountryCode());
            IAuthenticationResponse authnResponse = engine.validateUnmarshalledResponse(proxyServiceSamlResponse,
                    citizenIpAddress, serviceSkew, null);

            LOG.info(LoggingMarkerMDC.SAML_EXCHANGE, "Connector - Processing SAML Response to request with ID {}",
                    connectorRequestId);

            prepareRespLoggerBean(EIDASValues.EIDAS_CONNECTOR_RESPONSE.toString(), authnResponse,
                    connectorRequestId);
            saveLog(AUCONNECTORSAML.LOGGER_COM_RESP);

            checkAntiReplay(responseFromProxyService, connectorAuthnRequest, authnResponse);

            checkServiceCountryToCitizenCountry(responseFromProxyService, connectorAuthnRequest, authnResponse);

            if (!authnResponse.isFailure()) {
                checkResponseLoA(responseFromProxyService, connectorAuthnRequest, authnResponse);
                checkIdentifierFormat(authnResponse);
            }

            ILightRequest serviceProviderRequest = storedServiceProviderRequest.getRequest();
            String serviceProviderRequestSamlId = serviceProviderRequest.getId();

            LOG.trace("Checking status code");
            if (!EIDASStatusCode.SUCCESS_URI.toString().equals(authnResponse.getStatusCode())) {
                LOG.info("ERROR : Auth not succeed!");

                String errorCode = EIDASUtil.getEidasErrorCode(authnResponse.getStatusMessage());
                // We only change the error message if we get any error code on the Message!
                // Backwards compatibility
                String errorMessage = authnResponse.getStatusMessage();
                if (StringUtils.isNotBlank(errorCode)) {
                    errorMessage = extractErrorMessage(errorMessage, errorCode);
                }
                authnResponse = AuthenticationResponse.builder(authnResponse).statusMessage(errorMessage).build();
            }

            LOG.trace("Checking audience...");
            checkAudienceRestriction(connectorAuthnRequest.getIssuer(), authnResponse.getAudienceRestriction());

            AuthenticationResponse connectorResponse = new AuthenticationResponse.Builder(authnResponse)
                    .inResponseTo(serviceProviderRequestSamlId).issuer(getConnectorResponderMetadataUrl()).build();

            return new AuthenticationExchange(storedConnectorRequest, connectorResponse);

        } catch (EIDASSAMLEngineException e) {
            LOG.info("BUSINESS EXCEPTION : SAML validation error", e.getMessage());
            LOG.debug("BUSINESS EXCEPTION : SAML validation error", e);
            EidasNodeErrorUtil.processSAMLEngineException(e, LOG,
                    getConnectorRedirectError(e, EidasErrorKey.COLLEAGUE_RESP_INVALID_SAML));
            //normal processing of the above line will already cause the throw of the below exception
            throw new InternalErrorEIDASException(
                    EidasErrors.get(EidasErrorKey.COLLEAGUE_RESP_INVALID_SAML.errorCode()),
                    EidasErrors.get(EidasErrorKey.COLLEAGUE_RESP_INVALID_SAML.errorMessage()), e);
        }
    }

    private void validateAttributeValueFormat(String value, String currentAttrName, String attrNameToTest,
            String pattern) throws ValidationException {
        if (currentAttrName.equals(attrNameToTest) && !Pattern.matches(pattern, value)) {
            throw new ValidationException(attrNameToTest + " has incorrect format.");
        }

    }

    private void checkIdentifierFormat(IAuthenticationResponse authnResponse) throws InternalErrorEIDASException {
        String patterEidentifier = "^[A-Z]{2}/[A-Z]{2}/[A-Za-z0-9+/=\r\n]+$";
        if (authnResponse.getAttributes() != null) {
            ImmutableSet personIdentifier = authnResponse.getAttributes().getAttributeValuesByNameUri(
                    EidasSpec.Definitions.PERSON_IDENTIFIER.getNameUri().toASCIIString());
            if (personIdentifier != null && !personIdentifier.isEmpty()) {
                if (!Pattern.matches(patterEidentifier,
                        ((StringAttributeValue) personIdentifier.iterator().next()).getValue())) {
                    throw new InternalErrorEIDASException(EidasErrorKey.COLLEAGUE_RESP_INVALID_SAML.errorCode(),
                            "Person Identifier has an invalid format.");
                }
            }
            ImmutableSet legalPersonIdentifier = authnResponse.getAttributes().getAttributeValuesByNameUri(
                    EidasSpec.Definitions.LEGAL_PERSON_IDENTIFIER.getNameUri().toASCIIString());
            if (legalPersonIdentifier != null && !legalPersonIdentifier.isEmpty()) {
                if (!Pattern.matches(patterEidentifier,
                        ((StringAttributeValue) legalPersonIdentifier.iterator().next()).getValue())) {
                    throw new InternalErrorEIDASException(EidasErrorKey.COLLEAGUE_RESP_INVALID_SAML.errorCode(),
                            "Legal person Identifier has an invalid format.");
                }

            }
        }

    }

    /**
     * Compares the stored SAML request id to the incoming SAML response id.
     *
     * @param auRequestID The stored Id of the SAML request.
     * @param currentRequestId The Id of the incoming SAML response.
     */
    private void checkInResponseTo(String auRequestID, String currentRequestId) {

        if (auRequestID == null || !auRequestID.equals(currentRequestId)) {
            LOG.info(LoggingMarkerMDC.SECURITY_WARNING,
                    "ERROR : Stored request Id ({}) is not the same than response request id ({})", auRequestID,
                    currentRequestId);
            throw new InvalidSessionEIDASException(EidasErrors.get(EidasErrorKey.AU_REQUEST_ID.errorCode()),
                    EidasErrors.get(EidasErrorKey.AU_REQUEST_ID.errorMessage()));
        }
    }

    /**
     * Check if the citizen country code is the same than the Service signing certificate
     *
     * @param samlToken the samlToken received
     * @param spAuthnRequest the initial authnRequest
     * @param authnResponse the authnResponse
     */
    private void checkServiceCountryToCitizenCountry(byte[] samlToken, IAuthenticationRequest spAuthnRequest,
            IAuthenticationResponse authnResponse) {
        if (checkCitizenCertificateServiceCertificate
                && !spAuthnRequest.getCitizenCountryCode().equals(authnResponse.getCountry())) {
            LOG.warn("ERROR : Signing country for Service " + authnResponse.getCountry()
                    + " is not the same than the citizen country code " + spAuthnRequest.getCitizenCountryCode());
            prepareReqLoggerBean(EIDASValues.SP_REQUEST.toString(), samlToken, spAuthnRequest,
                    spAuthnRequest.getId());
            saveLog(AUCONNECTORSAML.LOGGER_COM_REQ);
            throw new InvalidSessionEIDASException(
                    EidasErrors.get(EidasErrorKey.INVALID_RESPONSE_COUNTRY_ISOCODE.errorCode()),
                    EidasErrors.get(EidasErrorKey.INVALID_RESPONSE_COUNTRY_ISOCODE.errorMessage()));
        }
    }

    /**
     * check the LoA in the response against connector's own LoA
     *
     * @param samlToken
     * @param spAuthnRequest
     * @param authnResponse
     */
    private void checkResponseLoA(byte[] samlToken, IAuthenticationRequest spAuthnRequest,
            IAuthenticationResponse authnResponse) {
        LevelOfAssurance requestedLevel = LevelOfAssurance.getLevel(spAuthnRequest.getLevelOfAssurance());
        LevelOfAssurance responseLevel = LevelOfAssurance.getLevel(authnResponse.getLevelOfAssurance());
        if (requestedLevel != null && (responseLevel == null
                || !EidasNodeValidationUtil.isRequestLoAValid(spAuthnRequest, responseLevel.stringValue()))) {
            LOG.info("ERROR : the level of assurance in the response " + authnResponse.getLevelOfAssurance()
                    + " does not satisfy the requested level " + requestedLevel);
            prepareReqLoggerBean(EIDASValues.SP_REQUEST.toString(), samlToken, spAuthnRequest,
                    spAuthnRequest.getId());
            saveLog(AUCONNECTORSAML.LOGGER_COM_REQ);
            throw new InvalidSessionEIDASException(EidasErrors.get(EidasErrorKey.INTERNAL_ERROR.errorCode()),
                    EidasErrors.get(EidasErrorKey.INTERNAL_ERROR.errorMessage()));
        }
    }

    /**
     * Check the antireplay cache to control if the samlId has not yet been submitted
     *
     * @param samlToken the samlToken received
     * @param spAuthnRequest the initial authnRequest
     * @param authnResponse the authnResponse
     */
    private void checkAntiReplay(byte[] samlToken, IAuthenticationRequest spAuthnRequest,
            IAuthenticationResponse authnResponse) {
        if (!connectorUtil.checkNotPresentInCache(authnResponse.getId(), authnResponse.getCountry())) {
            LOG.info("ERROR : SAMLID " + authnResponse.getId() + "+ for response found in Antireplay cache");
            prepareReqLoggerBean(EIDASValues.SP_REQUEST.toString(), samlToken, spAuthnRequest,
                    spAuthnRequest.getId());
            saveLog(AUCONNECTORSAML.LOGGER_COM_REQ);
            throw new SecurityEIDASException(
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML.errorCode()),
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML.errorMessage()));
        }
    }

    public String getIssuer() {
        String connectorMetadataUrl = getConnectorMetadataUrl();
        if (StringUtils.isNotBlank(connectorMetadataUrl) && PropertiesUtil.isMetadataEnabled()) {
            return connectorMetadataUrl;
        }
        ProtocolEngineI engine = getSamlEngine(samlSpInstance);
        return engine.getCoreProperties().getRequester();
    }

    /**
     * Generates a request SAML token based on an authentication request.
     *
     * @param instance String containing the SAML configuration to load.
     * @param authData An authentication request to generate the SAML token.
     * @return An authentication request with the embedded SAML token.
     * @see EidasAuthenticationRequest
     */
    private IRequestMessage generateAuthenticationRequest(@Nonnull String instance,
            @Nonnull IAuthenticationRequest request, @Nonnull String serviceCountryCode) {

        if (!(request instanceof IEidasAuthenticationRequest)) {
            // Send an error SAML message back - the use of InternalErrorEIDASException should have triggered an error page
            throw new InvalidParameterEIDASException(
                    EidasErrors.get(EidasErrorKey.MESSAGE_FORMAT_UNSUPPORTED.errorCode()),
                    EidasErrors.get(EidasErrorKey.MESSAGE_FORMAT_UNSUPPORTED.errorMessage()));
        }

        boolean modified = false;
        EidasAuthenticationRequest.Builder builder = null;
        IEidasAuthenticationRequest eidasRequest = (IEidasAuthenticationRequest) request;

        if (!EidasSamlBinding.EMPTY.getName().equals(eidasRequest.getBinding())) {
            builder = EidasAuthenticationRequest.builder(eidasRequest);
            builder.binding(EidasSamlBinding.EMPTY.getName());
            modified = true;
        }

        // If there is no SP Country, Then we get it from SAML's Certificate
        if (StringUtils.isBlank(request.getServiceProviderCountryCode())) {
            if (null == builder) {
                builder = EidasAuthenticationRequest.builder(eidasRequest);
            }
            builder.serviceProviderCountryCode(request.getOriginCountryCode());
            modified = true;
        }

        String connectorMetadataUrl = getConnectorMetadataUrl();
        if (connectorMetadataUrl != null && !connectorMetadataUrl.isEmpty() && PropertiesUtil.isMetadataEnabled()
                && !request.getIssuer().equals(connectorMetadataUrl)) {
            if (null == builder) {
                builder = EidasAuthenticationRequest.builder(eidasRequest);
            }
            builder.originalIssuer(request.getIssuer());
            builder.issuer(connectorMetadataUrl);
            modified = true;
        }

        try {
            ProtocolEngineI engine = getSamlEngine(instance);

            LOG.info(LoggingMarkerMDC.SAML_EXCHANGE, "Connector - Processing SAML Request with ID {}",
                    request.getId());

            String serviceMetadataURL = getConnectorUtil().loadConfigServiceMetadataURL(serviceCountryCode);
            if (StringUtils.isEmpty(serviceMetadataURL)) {
                String message = "The service metadata URL for \"" + serviceCountryCode + "\" is not configured";
                LOG.error(message);
                throw new InternalErrorEIDASException(
                        EidasErrors.get(EidasErrorKey.SAML_ENGINE_NO_METADATA.errorCode()),
                        EidasErrors.get(EidasErrorKey.SAML_ENGINE_NO_METADATA.errorMessage()), message);
            }

            if (modified) {
                request = builder.build();
            }

            return engine.generateRequestMessage(request, serviceMetadataURL);

        } catch (EIDASSAMLEngineException e) {
            LOG.info(instance + " : Error generating SAML Token", e.getMessage());
            LOG.debug(instance + " : Error generating SAML Token", e);
            EidasNodeErrorUtil.processSAMLEngineException(e, LOG,
                    getConnectorRedirectError(e, EidasErrorKey.SPROVIDER_SELECTOR_INVALID_SAML));
            throw new InternalErrorEIDASException(
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_ERROR_CREATE_SAML.errorCode()),
                    EidasErrors.get(EidasErrorKey.SPROVIDER_SELECTOR_ERROR_CREATE_SAML.errorMessage()), e);
        }
    }

    /**
     * Sets all the fields to audit the request.
     *
     * @param opType The operation type.
     * @param samlObj The SAML token byte[].
     * @param authnRequest The Authentication Request object.
     * @param spSamlId The SP's SAML ID.
     * @see EidasAuthenticationRequest
     */
    private void prepareReqLoggerBean(String opType, byte[] samlObj, IAuthenticationRequest authnRequest,
            String spSamlId) {
        String hashClassName = getConnectorUtil() != null && getConnectorUtil().getConfigs() != null
                ? getConnectorUtil().getConfigs().getProperty(EidasParameterKeys.HASH_DIGEST_CLASS.toString())
                : null;
        byte[] tokenHash = EidasDigestUtil.hashPersonalToken(samlObj, hashClassName);
        loggerBean.setTimestamp(DateUtil.currentTimeStamp().toString());
        loggerBean.setOpType(opType);
        loggerBean.setOrigin(authnRequest.getAssertionConsumerServiceURL());
        loggerBean.setDestination(authnRequest.getDestination());
        loggerBean.setProviderName(authnRequest.getProviderName());
        loggerBean.setCountry(authnRequest.getCitizenCountryCode());
        if (authnRequest instanceof IStorkAuthenticationRequest) {
            IStorkAuthenticationRequest storkAuthenticationRequest = (IStorkAuthenticationRequest) authnRequest;
            loggerBean.setSpApplication(storkAuthenticationRequest.getSpApplication());
            loggerBean.setQaaLevel(storkAuthenticationRequest.getQaa());
        }
        loggerBean.setSamlHash(tokenHash);
        loggerBean.setSPMsgId(spSamlId);
        loggerBean.setMsgId(authnRequest.getId());
    }

    /**
     * Sets all the fields to the audit the response.
     *
     * @param opType The Operation Type.
     * @param authnResponse The Authentication Response object.
     * @param inResponseToSPReq The SP's SAML Id.
     * @see EidasAuthenticationRequest
     */
    private void prepareRespLoggerBean(String opType, IAuthenticationResponse authnResponse,
            String inResponseToSPReq) {
        String message = EIDASValues.SUCCESS.toString() + EIDASValues.EID_SEPARATOR.toString()
                + EIDASValues.CITIZEN_CONSENT_LOG.toString();
        loggerBean.setTimestamp(DateUtil.currentTimeStamp().toString());
        loggerBean.setOpType(opType);
        loggerBean.setInResponseTo(authnResponse.getInResponseToId());
        loggerBean.setInResponseToSPReq(inResponseToSPReq);
        loggerBean.setMessage(message);
        loggerBean.setMsgId(authnResponse.getId());
    }

    /**
     * Logs the transaction with the Audit log.
     *
     * @param logger The Audit Logger.
     */
    public void saveLog(Logger logger) {
        logger.info(LoggingMarkerMDC.SAML_EXCHANGE, loggerBean.toString());
    }

    /**
     * Setters and getters
     */

    /**
     * Setter for loggerBean.
     *
     * @param nLoggerBean The loggerBean to set.
     * @see IEIDASLogger
     */
    public void setLoggerBean(IEIDASLogger nLoggerBean) {
        this.loggerBean = nLoggerBean;
    }

    /**
     * Getter for loggerBean.
     *
     * @return The loggerBean value.
     * @see IEIDASLogger
     */
    public IEIDASLogger getLoggerBean() {
        return loggerBean;
    }

    /**
     * Compares the issuer to the audience restriction.
     *
     * @param issuer The stored SAML request issuer.
     * @param audience The SAML response audience.
     */
    private void checkAudienceRestriction(String issuer, String audience) {

        if (issuer == null || !issuer.equals(audience)) {
            LOG.info("ERROR : Audience is null or not valid: audienceRestriction=\"" + audience + "\" vs issuer=\""
                    + issuer + "\"");
            throw new InvalidSessionEIDASException(EidasErrors.get(EidasErrorKey.AUDIENCE_RESTRICTION.errorCode()),
                    EidasErrors.get(EidasErrorKey.AUDIENCE_RESTRICTION.errorMessage()));
        }
    }

    @Override
    public boolean checkMandatoryAttributes(@Nonnull ImmutableAttributeMap attributes) {
        ProtocolEngineI engine = getSamlEngine(samlSpInstance);
        return engine.getProtocolProcessor().checkMandatoryAttributes(attributes);
    }

    // TODO why are there 2 SAML engines in the connector?
    public ProtocolEngineI getSamlEngine(@Nonnull String instanceName) {
        return nodeProtocolEngineFactory.getProtocolEngine(instanceName);
    }

    /**
     * Setter for samlSpInstance.
     *
     * @param nSamlSpInstance The new SamlSpInstance value.
     */
    public void setSamlSpInstance(String nSamlSpInstance) {
        this.samlSpInstance = nSamlSpInstance;
    }

    /**
     * Getter for samlSpInstance.
     *
     * @return The samlSpInstance value.
     */
    public String getSamlSpInstance() {
        return samlSpInstance;
    }

    /**
     * Setter for samlServiceInstance.
     *
     * @param samlServiceInstance The new samlServiceInstance value.
     */
    public void setSamlServiceInstance(String samlServiceInstance) {
        this.samlServiceInstance = samlServiceInstance;
    }

    /**
     * Setter for connectorUtil.
     *
     * @param connectorUtil The new connectorUtil value.
     * @see AUCONNECTORUtil
     */
    public void setConnectorUtil(AUCONNECTORUtil connectorUtil) {
        this.connectorUtil = connectorUtil;
    }

    /**
     * Getter for connectorUtil.
     *
     * @return The connectorUtil value.
     * @see AUCONNECTORUtil
     */
    public AUCONNECTORUtil getConnectorUtil() {
        return connectorUtil;
    }

    /**
     * Setter for messageSource.
     *
     * @param nMessageSource The new messageSource value.
     * @see MessageSource
     */
    public void setMessageSource(MessageSource nMessageSource) {
        this.messageSource = nMessageSource;
    }

    private String getConnectorMetadataUrl() {
        return metadataUrl;
    }

    public void setConnectorMetadataUrl(String metadataUrl) {
        this.metadataUrl = metadataUrl;
    }

    public MetadataFetcherI getMetadataFetcher() {
        return metadataFetcher;
    }

    public void setMetadataFetcher(MetadataFetcherI metadataFetcher) {
        this.metadataFetcher = metadataFetcher;
    }

    public String getConnectorResponderMetadataUrl() {
        return metadataResponderUrl;
    }

    public void setConnectorResponderMetadataUrl(String metadataResponderUrl) {
        this.metadataResponderUrl = metadataResponderUrl;
    }

    public void setNodeProtocolEngineFactory(ProtocolEngineFactory nodeProtocolEngineFactory) {
        this.nodeProtocolEngineFactory = nodeProtocolEngineFactory;
    }
}