package at.asitplus.eidas.specific.modules.msproxyservice.protocol; import java.io.IOException; import java.text.MessageFormat; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.opensaml.saml.saml2.core.NameIDType; import org.opensaml.saml.saml2.core.StatusCode; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Streams; import at.asitplus.eidas.specific.core.MsEidasNodeConstants; import at.asitplus.eidas.specific.core.MsEventCodes; import at.asitplus.eidas.specific.core.config.ServiceProviderConfiguration; import at.asitplus.eidas.specific.modules.core.eidas.EidasConstants; import at.asitplus.eidas.specific.modules.msproxyservice.MsProxyServiceConstants; import at.asitplus.eidas.specific.modules.msproxyservice.exception.EidasProxyServiceException; import at.asitplus.eidas.specific.modules.msproxyservice.handler.IEidasAttributeHandler; import at.asitplus.eidas.specific.modules.msproxyservice.service.ProxyEidasAttributeRegistry; import at.asitplus.eidas.specific.modules.msproxyservice.utils.EidasProxyServiceUtils; import at.gv.egiz.components.eventlog.api.EventConstants; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.data.EaafConfigConstants; import at.gv.egiz.eaaf.core.api.data.EaafConstants; import at.gv.egiz.eaaf.core.api.data.ExtendedPvpAttributeDefinitions.SpMandateModes; import at.gv.egiz.eaaf.core.api.idp.IModulInfo; import at.gv.egiz.eaaf.core.api.idp.ISpConfiguration; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.GuiBuildException; import at.gv.egiz.eaaf.core.impl.idp.controller.AbstractController; import at.gv.egiz.eaaf.core.impl.utils.KeyValueUtils; import eu.eidas.auth.commons.EIDASSubStatusCode; import eu.eidas.auth.commons.EidasParameterKeys; import eu.eidas.auth.commons.light.ILightRequest; import eu.eidas.auth.commons.light.impl.LightResponse; import eu.eidas.auth.commons.light.impl.LightResponse.Builder; import eu.eidas.auth.commons.light.impl.ResponseStatus; import eu.eidas.specificcommunication.exception.SpecificCommunicationException; import eu.eidas.specificcommunication.protocol.SpecificCommunicationService; import lombok.extern.slf4j.Slf4j; /** * End-point implementation for authentication requests from eIDAS Proxy-Service * to MS-specific eIDAS Proxy-Service. * * @author tlenz * */ @Slf4j @Controller public class EidasProxyServiceController extends AbstractController implements IModulInfo { private static final String ERROR_01 = "eidas.proxyservice.01"; private static final String ERROR_02 = "eidas.proxyservice.02"; private static final String ERROR_03 = "eidas.proxyservice.03"; private static final String ERROR_04 = "eidas.proxyservice.04"; private static final String ERROR_05 = "eidas.proxyservice.05"; private static final String ERROR_07 = "eidas.proxyservice.07"; private static final String ERROR_08 = "eidas.proxyservice.08"; private static final String ERROR_09 = "eidas.proxyservice.09"; private static final String ERROR_10 = "eidas.proxyservice.10"; private static final String ERROR_11 = "eidas.proxyservice.11"; public static final String PROTOCOL_ID = "eidasProxy"; @Autowired ProxyEidasAttributeRegistry attrRegistry; @Autowired ProxyServiceAuthenticationAction responseAction; /** * End-point that receives authentication requests from eIDAS Node. * * @param httpReq Http request * @param httpResp Http response * @throws IOException In case of general error * @throws EaafException In case of a validation or processing error */ @RequestMapping(value = { MsProxyServiceConstants.EIDAS_HTTP_ENDPOINT_IDP_POST, MsProxyServiceConstants.EIDAS_HTTP_ENDPOINT_IDP_REDIRECT }, method = { RequestMethod.POST, RequestMethod.GET }) public void receiveEidasAuthnRequest(HttpServletRequest httpReq, HttpServletResponse httpResp) throws IOException, EaafException { log.trace("Receive request on eidas proxy-service end-points"); ProxyServicePendingRequest pendingReq = null; try { // get token from Request final String tokenBase64 = httpReq.getParameter(EidasParameterKeys.TOKEN.toString()); if (StringUtils.isEmpty(tokenBase64)) { log.warn("NO eIDAS message token found."); throw new EidasProxyServiceException(ERROR_02, null); } log.trace("Receive eIDAS-node token: {}. Searching authentication request from eIDAS Proxy-Service ...", tokenBase64); // read authentication request from shared cache final SpecificCommunicationService specificProxyCommunicationService = (SpecificCommunicationService) applicationContext.getBean( EidasConstants.SPECIFIC_PROXYSERVICE_COMMUNICATION_SERVICE); final ILightRequest eidasRequest = specificProxyCommunicationService.getAndRemoveRequest( tokenBase64, ImmutableSortedSet.copyOf(attrRegistry.getCoreRegistry().getCoreAttributeRegistry() .getAttributes())); if (eidasRequest == null) { log.info("Find no eIDAS Authn. Request with stated token."); throw new EidasProxyServiceException(ERROR_11, null); } log.debug("Received eIDAS auth. request from: {}, Initializing authentication environment ... ", eidasRequest.getSpCountryCode() != null ? eidasRequest.getSpCountryCode() : "'missing SP-country'"); log.trace("Received eIDAS requst: {}", eidasRequest); // create pendingRequest object pendingReq = applicationContext.getBean(ProxyServicePendingRequest.class); pendingReq.initialize(httpReq, authConfig); pendingReq.setModule(getName()); // log 'transaction created' event revisionsLogger.logEvent(EventConstants.TRANSACTION_CREATED, pendingReq.getUniqueTransactionIdentifier()); revisionsLogger.logEvent(pendingReq.getUniqueSessionIdentifier(), pendingReq.getUniqueTransactionIdentifier(), EventConstants.TRANSACTION_IP, httpReq.getRemoteAddr()); revisionsLogger.logEvent(pendingReq, MsEventCodes.EIDAS_OUTGOING_RECEIVED, eidasRequest.getId()); // validate eIDAS Authn. request and set into pending-request validateEidasAuthnRequest(eidasRequest); pendingReq.setEidasRequest(eidasRequest); // generate Service-Provider configuration from eIDAS request final ISpConfiguration spConfig = generateSpConfigurationFromEidasRequest(eidasRequest); // validate eIDAS Authn. request by using eIDAS Connector specifc parameters validateEidasAuthnRequest(spConfig, eidasRequest); // populate pendingRequest with parameters pendingReq.setOnlineApplicationConfiguration(spConfig); pendingReq.setSpEntityId(spConfig.getUniqueIdentifier()); pendingReq.setPassiv(false); pendingReq.setForce(true); // AuthnRequest needs authentication pendingReq.setNeedAuthentication(true); // set protocol action, which should be executed after authentication pendingReq.setAction(ProxyServiceAuthenticationAction.class.getName()); // switch to session authentication protAuthService.performAuthentication(httpReq, httpResp, pendingReq); } catch (final EidasProxyServiceException e) { throw e; } catch (final SpecificCommunicationException e) { log.error("Can not read eIDAS Authn request from shared cache. Reason: {}", e.getMessage()); throw new EidasProxyServiceException(ERROR_03, new Object[] { e.getMessage() }, e); } catch (final Throwable e) { // write revision log entries if (pendingReq != null) { revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); } throw new EidasProxyServiceException(ERROR_01, new Object[] { e.getMessage() }, e); } } @Override public boolean generateErrorMessage(Throwable e, HttpServletRequest httpReq, HttpServletResponse httpResp, IRequest pendingReq) throws Throwable { return authConfig.getBasicConfigurationBoolean( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_NODE_FORWARD_ERRORS, false) && generateAndSendError(e, httpReq, httpResp, pendingReq); } @Override public String getName() { return EidasProxyServiceController.class.getName(); } @Override public String getAuthProtocolIdentifier() { return PROTOCOL_ID; } @Override public boolean validate(HttpServletRequest request, HttpServletResponse response, IRequest pending) { return true; } /** * Generic validation of incoming eIDAS request. * * @param eidasRequest Incoming eIDAS authentication request * @throws EidasProxyServiceException In case of a validation error */ private void validateEidasAuthnRequest(ILightRequest eidasRequest) throws EidasProxyServiceException { if (StringUtils.isEmpty(eidasRequest.getIssuer())) { throw new EidasProxyServiceException(ERROR_05, null); } // perform advanced request validation Set requiredHandlers = eidasRequest.getRequestedAttributes().getAttributeMap().keySet().stream() .map(el -> attrRegistry.mapEidasAttributeToAttributeHandler(el.getNameUri().toString()).orElse(null)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toSet()); if (!requiredHandlers.isEmpty()) { log.info("eIDAS requested attributes requires #{} specific attribute-hander. " + "Starting advanced attribute-validation ... ", requiredHandlers.size()); for (String el : requiredHandlers) { executeAdvancedRequestValidation(el, eidasRequest); } } else { log.debug("No advanced eIDAS attribute-validation required."); } } /** * eIDAS Connector specific validation of incoming eIDAS request. * * @param eidasRequest Incoming eIDAS authentication request * @param spConfig eIDAS Connector configuration * @throws EidasProxyServiceException In case of a validation error */ private void validateEidasAuthnRequest(ISpConfiguration spConfig, ILightRequest eidasRequest) throws EidasProxyServiceException { // check if natural-person and legal-person attributes requested in parallel if (spConfig.isConfigurationValue(MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_VALIDATION_ATTR_MDS, true) && EidasProxyServiceUtils.isLegalPersonRequested(eidasRequest) && EidasProxyServiceUtils.isNaturalPersonRequested(eidasRequest)) { throw new EidasProxyServiceException(ERROR_08, null); } // TODO: validate some other stuff } /** * Generate a dummy Service-Provider configuration for processing. * * @param eidasRequest Incoming eIDAS authentication request * @return Service-Provider configuration that can be used for authentication * @throws EidasProxyServiceException In case of a configuration error */ private ISpConfiguration generateSpConfigurationFromEidasRequest(ILightRequest eidasRequest) throws EidasProxyServiceException { try { final Map connectorConfigMap = extractRawConnectorConfiguration(eidasRequest); // check if country-code is available final String spCountry = connectorConfigMap.get( MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_COUNTRYCODE); if (StringUtils.isEmpty(spCountry)) { throw new EidasProxyServiceException(ERROR_07, null); } // build FriendyName from CountryCode and SPType connectorConfigMap.put(MsEidasNodeConstants.PROP_CONFIG_SP_FRIENDLYNAME, MessageFormat.format(MsProxyServiceConstants.TEMPLATE_SP_UNIQUE_ID, spCountry, eidasRequest.getSpType())); // build Service-Provider configuration object final ServiceProviderConfiguration spConfig = new ServiceProviderConfiguration(connectorConfigMap, authConfig); // build bPK target from Country-Code final String ccCountry = authConfig.getBasicConfiguration( EidasConstants.CONIG_PROPS_EIDAS_NODE_COUNTRYCODE, EidasConstants.DEFAULT_MS_NODE_COUNTRY_CODE); spConfig.setBpkTargetIdentifier( EaafConstants.URN_PREFIX_EIDAS + ccCountry + "+" + spCountry); // set required LoA from eIDAS request spConfig.setRequiredLoA( eidasRequest.getLevelsOfAssurance().stream().map(el -> el.getValue()).collect(Collectors.toList())); // build mandate profiles for this specific request buildMandateProfileConfiguration(spConfig, eidasRequest); // map eIDAS attributes to national attributes buildNationalRequestedAttributes(spConfig, eidasRequest); // execute custom attribute-handler advancedAttributeHandler(spConfig, eidasRequest); return spConfig; } catch (final EidasProxyServiceException e) { throw e; } catch (final EaafException e) { throw new EidasProxyServiceException(ERROR_04, new Object[] { e.getMessage() }, e); } } private void advancedAttributeHandler(ServiceProviderConfiguration spConfig, ILightRequest eidasRequest) { Set requiredHandlers = eidasRequest.getRequestedAttributes().getAttributeMap().keySet().stream() .map(el -> attrRegistry.mapEidasAttributeToAttributeHandler(el.getNameUri().toString()).orElse(null)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toSet()); if (!requiredHandlers.isEmpty()) { log.info("eIDAS requested attributes requires #{} specific attribute-hander. " + "Starting advanced attribute-handling ... ", requiredHandlers.size()); requiredHandlers.forEach(el -> executeAttributeHandler(el, spConfig)); } else { log.debug("No advanced eIDAS attribute-handling required."); } } private void executeAttributeHandler(String handlerClass, ServiceProviderConfiguration spConfig) { try { IEidasAttributeHandler handler = applicationContext.getBean(handlerClass, IEidasAttributeHandler.class); log.trace("Perfom SP config post-processing by using: {}", handler.getClass().getName()); handler.performSpConfigPostprocessing(spConfig); } catch (Exception e) { log.error("No custom attribute-handler implementation for: {}. Operation can NOT be performed", handlerClass, e); } } private void executeAdvancedRequestValidation(String handlerClass, ILightRequest eidasRequest) throws EidasProxyServiceException { try { IEidasAttributeHandler handler = applicationContext.getBean(handlerClass, IEidasAttributeHandler.class); log.trace("Perfom request-validastion by using: {}", handler.getClass().getName()); handler.validateAuthnRequest(eidasRequest); } catch (BeansException e) { log.error("No custom attribute-handler implementation for: {}. Operation can NOT be performed", handlerClass, e); } } private void buildNationalRequestedAttributes( ServiceProviderConfiguration spConfig, ILightRequest eidasRequest) { final boolean mandatesEnabled = !SpMandateModes.NONE.equals(spConfig.getMandateMode()); spConfig.setRequestedAttributes( Streams.concat( eidasRequest.getRequestedAttributes().getAttributeMap().keySet().stream() .map(el -> attrRegistry.getIdaAttributesForEidasAttribute( el.getNameUri().toString(), mandatesEnabled)) .flatMap(Collection::stream) .filter(Objects::nonNull), attrRegistry.getAlwaysRequestedAttributes(mandatesEnabled)) .collect(Collectors.toSet())); log.debug("Inject #{} attributes to request from IDA system", spConfig.getRequestedAttributes().size()); } private Map extractRawConnectorConfiguration(ILightRequest eidasRequest) { final Map allConnectorConfigs = authConfig.getBasicConfigurationWithPrefix( MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_PREFIX); if (log.isTraceEnabled()) { log.trace("Full-connector configuration:"); allConnectorConfigs.entrySet().stream().forEach( el -> log.trace("Key: {} -> Value: {}", el.getKey(), el.getValue())); } final Map connectorConfig = allConnectorConfigs.entrySet().stream() .filter(el -> el.getKey().endsWith(MsEidasNodeConstants.PROP_CONFIG_SP_UNIQUEIDENTIFIER) && el.getValue().equals(eidasRequest.getIssuer())) .findFirst() .map(el -> KeyValueUtils.getSubSetWithPrefix(allConnectorConfigs, KeyValueUtils.getParentKey(el.getKey()) + KeyValueUtils.KEY_DELIMITER)) .orElse(new HashMap<>()); if (connectorConfig.isEmpty()) { log.debug("No specific configuration for eIDAS Connector: {} Using default configuration ... ", eidasRequest.getIssuer()); // set EntityId of the requesting eIDAS Connector connectorConfig.put(EaafConfigConstants.SERVICE_UNIQUEIDENTIFIER, eidasRequest.getIssuer()); // set country-code from eIDAS request connectorConfig.put(MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_COUNTRYCODE, eidasRequest.getSpCountryCode()); // set default mandate configuration connectorConfig.put(MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_MANDATES_ENABLED, String.valueOf(authConfig.getBasicConfigurationBoolean( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_MANDATES_ENABLED, false))); connectorConfig.put(MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_MANDATES_PROFILE_NATURAL, authConfig.getBasicConfiguration( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_MANDATES_PROFILE_DEFAULT_NATURAL)); connectorConfig.put(MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_MANDATES_PROFILE_LEGAL, authConfig.getBasicConfiguration( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_MANDATES_PROFILE_DEFAULT_LEGAL)); } else { log.debug("Find specific configuration for eIDAS Connector: {}", eidasRequest.getIssuer()); } return connectorConfig; } private void buildMandateProfileConfiguration(ServiceProviderConfiguration spConfig, ILightRequest eidasRequest) throws EidasProxyServiceException { // check if mandates are enabled if (spConfig.isConfigurationValue(MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_MANDATES_ENABLED, false)) { injectMandateInfosIntoSpConfig(spConfig, eidasRequest); } else { if (EidasProxyServiceUtils.isLegalPersonRequested(eidasRequest)) { throw new EidasProxyServiceException(ERROR_09, null); } spConfig.setMandateProfiles(Collections.emptyList()); spConfig.setMandateMode(SpMandateModes.NONE); } } private void injectMandateInfosIntoSpConfig(ServiceProviderConfiguration spConfig, ILightRequest eidasRequest) throws EidasProxyServiceException { log.trace("eIDAS Proxy-Service allows mandates for Connector: {}. Selecting profiles ... ", spConfig.getUniqueIdentifier()); final List legalPersonProfiles = KeyValueUtils.getListOfCsvValues(spConfig.getConfigurationValue( MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_MANDATES_PROFILE_LEGAL)); final List natPersonProfiles = KeyValueUtils.getListOfCsvValues(spConfig.getConfigurationValue( MsProxyServiceConstants.CONIG_PROPS_CONNECTOR_MANDATES_PROFILE_NATURAL)); if (EidasProxyServiceUtils.isLegalPersonRequested(eidasRequest) && EidasProxyServiceUtils.isNaturalPersonRequested(eidasRequest)) { log.debug( "Find requested attributes for legal and natural persons. Injecting mandate-profiles for both ... "); spConfig.setMandateProfiles(ListUtils.union(natPersonProfiles, legalPersonProfiles)); // set Mandate-Mode based on SP configuration final boolean isLegalPersonProfile = !legalPersonProfiles.isEmpty(); final boolean isNaturalPersonProfile = !natPersonProfiles.isEmpty(); spConfig.setMandateMode( isLegalPersonProfile && isNaturalPersonProfile ? SpMandateModes.BOTH : isLegalPersonProfile ? SpMandateModes.LEGAL_FORCE : SpMandateModes.NATURAL); } else if (EidasProxyServiceUtils.isLegalPersonRequested(eidasRequest)) { // check if legal person is requested spConfig.setMandateProfiles(legalPersonProfiles); spConfig.setMandateMode(SpMandateModes.LEGAL_FORCE); if (spConfig.getMandateProfiles().isEmpty()) { throw new EidasProxyServiceException(ERROR_10, null); } } else if (EidasProxyServiceUtils.isNaturalPersonRequested(eidasRequest)) { spConfig.setMandateProfiles(natPersonProfiles); spConfig.setMandateMode(SpMandateModes.NATURAL); } if (spConfig.getMandateProfiles().isEmpty()) { log.debug("No mandate-profiles for issure: {}. Set mandate-mode to 'none'", spConfig.getUniqueIdentifier()); spConfig.setMandateMode(SpMandateModes.NONE); } else { log.debug("Set mandate-profiles: {} to request from issuer: {}", spConfig.getMandateProfiles(), spConfig.getUniqueIdentifier()); } } private boolean generateAndSendError(Throwable e, HttpServletRequest httpReq, HttpServletResponse httpResp, IRequest pendingReq) throws EaafConfigurationException { if (pendingReq instanceof ProxyServicePendingRequest) { try { final ILightRequest eidasReq = ((ProxyServicePendingRequest) pendingReq).getEidasRequest(); // build eIDAS response final Builder lightRespBuilder = LightResponse.builder(); lightRespBuilder.id(UUID.randomUUID().toString()); lightRespBuilder.inResponseToId(eidasReq.getId()); lightRespBuilder.relayState(eidasReq.getRelayState()); lightRespBuilder.issuer(authConfig.getBasicConfiguration( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_NODE_ENTITYID)); lightRespBuilder.subject(UUID.randomUUID().toString()); lightRespBuilder.subjectNameIdFormat(NameIDType.TRANSIENT); lightRespBuilder.status(ResponseStatus.builder() .statusCode(StatusCode.RESPONDER) .subStatusCode(EIDASSubStatusCode.AUTHN_FAILED_URI.getValue()) .statusMessage(StringEscapeUtils.escapeXml(e.getLocalizedMessage())) .build()); // forward to eIDAS Proxy-Service responseAction.forwardToEidasProxy(pendingReq, httpReq, httpResp, lightRespBuilder.build()); return true; } catch (ServletException | IOException | GuiBuildException e1) { log.warn("Forward error to eIDAS Proxy-Service FAILED. Handle error localy ... ", e1); } } else { log.error("eIDAS Proxy-Service authentication requires PendingRequest of Type: {}", ProxyServicePendingRequest.class.getName()); } return false; } }