/******************************************************************************* *******************************************************************************/ package at.asitplus.eidas.specific.modules.authmodule_eIDASv2.tasks; import java.util.Map; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; import com.google.common.collect.ImmutableSortedSet; import at.asitplus.eidas.specific.connector.MSConnectorEventCodes; import at.asitplus.eidas.specific.connector.MSeIDASNodeConstants; import at.asitplus.eidas.specific.connector.gui.StaticGuiBuilderConfiguration; import at.asitplus.eidas.specific.modules.authmodule_eIDASv2.Constants; import at.asitplus.eidas.specific.modules.authmodule_eIDASv2.exception.eIDASAuthenticationException; import at.asitplus.eidas.specific.modules.authmodule_eIDASv2.service.eIDASAttributeRegistry; import at.gv.egiz.eaaf.core.api.data.EAAFConstants; import at.gv.egiz.eaaf.core.api.gui.IGUIFormBuilder; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; import at.gv.egiz.eaaf.core.api.idp.ISPConfiguration; import at.gv.egiz.eaaf.core.api.idp.process.ExecutionContext; import at.gv.egiz.eaaf.core.api.storage.ITransactionStorage; import at.gv.egiz.eaaf.core.exceptions.EAAFConfigurationException; import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; import at.gv.egiz.eaaf.core.impl.idp.auth.modules.AbstractAuthServletTask; import eu.eidas.auth.commons.EidasParameterKeys; import eu.eidas.auth.commons.attribute.AttributeDefinition; import eu.eidas.auth.commons.attribute.ImmutableAttributeMap; import eu.eidas.auth.commons.light.ILightRequest; import eu.eidas.auth.commons.light.impl.LightRequest; import eu.eidas.auth.commons.protocol.eidas.SpType; import eu.eidas.auth.commons.tx.BinaryLightToken; import eu.eidas.specificcommunication.BinaryLightTokenHelper; import eu.eidas.specificcommunication.SpecificCommunicationDefinitionBeanNames; import eu.eidas.specificcommunication.exception.SpecificCommunicationException; import eu.eidas.specificcommunication.protocol.impl.SpecificConnectorCommunicationServiceImpl; /** * @author tlenz * */ @Component("ConnecteIDASNodeTask") public class GenerateAuthnRequestTask extends AbstractAuthServletTask { private static final Logger log = LoggerFactory.getLogger(GenerateAuthnRequestTask.class); @Autowired IConfiguration basicConfig; @Autowired eIDASAttributeRegistry attrRegistry; @Autowired ApplicationContext context; @Autowired ITransactionStorage transactionStore; @Autowired IGUIFormBuilder guiBuilder; @Override public void execute(ExecutionContext executionContext, HttpServletRequest request, HttpServletResponse response) throws TaskExecutionException { try{ //get service-provider configuration ISPConfiguration spConfig = pendingReq.getServiceProviderConfiguration(); // get target, environment and validate citizen countryCode String citizenCountryCode = (String) executionContext.get(MSeIDASNodeConstants.REQ_PARAM_SELECTED_COUNTRY); String environment = (String) executionContext.get(MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT); if (StringUtils.isEmpty(citizenCountryCode)) { // illegal state; task should not have been executed without a selected country throw new eIDASAuthenticationException("eidas.03", new Object[] { "" }); } //TODO: maybe add countryCode validation before request ref. impl. eIDAS node log.debug("Request eIDAS auth. for citizen of country: " + citizenCountryCode); revisionsLogger.logEvent(pendingReq, MSConnectorEventCodes.COUNTRY_SELECTED, citizenCountryCode); //build eIDAS AuthnRequest LightRequest.Builder authnRequestBuilder = LightRequest.builder(); authnRequestBuilder.id(UUID.randomUUID().toString()); String issur = basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_ENTITYID); if (StringUtils.isEmpty(issur)) { log.error("Found NO 'eIDAS node issuer' in configuration. Authentication NOT possible!"); throw new EAAFConfigurationException("config.27", new Object[] {"Application config containts NO " + Constants.CONIG_PROPS_EIDAS_NODE_ENTITYID }); } authnRequestBuilder.issuer(issur); //TODO: set matching mode if eIDAS ref. impl. support this method //TODO: update if eIDAS ref. impl. supports exact matching for non-notified LoA schemes String loa = EAAFConstants.EIDAS_LOA_HIGH; if (spConfig.getRequiredLoA() != null) { if (spConfig.getRequiredLoA().isEmpty()) log.info("No eIDAS LoA requested. Use LoA HIGH as default"); else { if (spConfig.getRequiredLoA().size() > 1 ) log.info("Currently only ONE requested LoA is supported for service provider. Use first one ... "); loa = spConfig.getRequiredLoA().get(0); } } log.debug("Request eIdAS node with LoA: " + loa); authnRequestBuilder.levelOfAssurance(loa); //set correct SPType for requested target sector String publicSectorTargetSelector = basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_NODE_PUBLICSECTOR_TARGETS, Constants.POLICY_DEFAULT_ALLOWED_TARGETS); Pattern p = Pattern.compile(publicSectorTargetSelector); Matcher m = p.matcher(spConfig.getAreaSpecificTargetIdentifier()); if (m.matches()) { log.debug("Map " + spConfig.getAreaSpecificTargetIdentifier() + " to 'PublicSector'"); authnRequestBuilder.spType(SpType.PUBLIC.getValue()); //TODO: only for eIDAS ref. node 2.0 and 2.1 because it need 'Providername' for any SPType String providerName = pendingReq.getRawData(Constants.DATA_PROVIDERNAME, String.class); if (StringUtils.isNotEmpty(providerName) && basicConfig.getBasicMOAIDConfigurationBoolean( Constants.CONIG_PROPS_EIDAS_NODE_WORKAROUND_ADD_ALWAYS_PROVIDERNAME, false) ) authnRequestBuilder.providerName(providerName); } else { log.debug("Map " + spConfig.getAreaSpecificTargetIdentifier() + " to 'PrivateSector'"); authnRequestBuilder.spType(SpType.PRIVATE.getValue()); //TODO: switch to RequesterId in further version //set provider name for private sector applications String providerName = pendingReq.getRawData(Constants.DATA_PROVIDERNAME, String.class); if (StringUtils.isNotEmpty(providerName)) authnRequestBuilder.providerName(providerName); } //set nameIDFormat authnRequestBuilder.nameIdFormat(Constants.eIDAS_REQ_NAMEID_FORMAT); //set citizen country code for foreign uses authnRequestBuilder.citizenCountryCode(citizenCountryCode); //set relay state authnRequestBuilder.relayState(pendingReq.getPendingRequestId()); //build and add requested attribute set ImmutableAttributeMap reqAttrMap = translateToEidasAttributes(attrRegistry.getAttributeSetFromConfiguration()); authnRequestBuilder.requestedAttributes(reqAttrMap); //build request LightRequest lightAuthnReq = authnRequestBuilder.build(); //put request into cache BinaryLightToken token = putRequestInCommunicationCache(lightAuthnReq); final String tokenBase64 = BinaryLightTokenHelper.encodeBinaryLightTokenBase64(token); //Workaround, because eIDAS node ref. impl. does not return relayState if (basicConfig.getBasicMOAIDConfigurationBoolean( Constants.CONIG_PROPS_EIDAS_NODE_WORKAROUND_USEREQUESTIDASTRANSACTIONIDENTIFIER, false)) { log.trace("Put lightRequestId into transactionstore as session-handling backup"); transactionStore.put(lightAuthnReq.getId(), pendingReq.getPendingRequestId(), -1); } //select forward URL regarding the selected environment String forwardURL = basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_URL); if (StringUtils.isNotEmpty(environment)) forwardURL = selectedForwardURLForEnvironment(environment); if (StringUtils.isEmpty(forwardURL)) { log.warn("NO ForwardURL defined in configuration. Can NOT forward to eIDAS node! Process stops"); throw new EAAFConfigurationException("config.08", new Object[] {Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_URL}); } log.debug("ForwardURL: " + forwardURL + " selected to forward eIDAS request"); if (basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_METHOD, Constants.FORWARD_METHOD_GET ).equals(Constants.FORWARD_METHOD_GET)) { log.debug("Use http-redirect for eIDAS node forwarding ... "); //send redirect UriComponentsBuilder redirectUrl = UriComponentsBuilder.fromHttpUrl(forwardURL); redirectUrl.queryParam(EidasParameterKeys.TOKEN.toString(), tokenBase64); response.sendRedirect(redirectUrl.build().encode().toString()); } else { log.debug("Use http-post for eIDAS node forwarding ... "); StaticGuiBuilderConfiguration config = new StaticGuiBuilderConfiguration( basicConfig, pendingReq, Constants.TEMPLATE_POST_FORWARD_NAME, null); config.putCustomParameter(Constants.TEMPLATE_POST_FORWARD_ENDPOINT, forwardURL); config.putCustomParameter(Constants.TEMPLATE_POST_FORWARD_TOKEN_NAME, EidasParameterKeys.TOKEN.toString()); config.putCustomParameter(Constants.TEMPLATE_POST_FORWARD_TOKEN_VALUE, tokenBase64); guiBuilder.build(response, config, "BKU-Selection form"); } revisionsLogger.logEvent(pendingReq, MSConnectorEventCodes.EIDAS_NODE_CONNECTED, lightAuthnReq.getId()); } catch (eIDASAuthenticationException e) { throw new TaskExecutionException(pendingReq, "eIDAS AuthnRequest generation FAILED.", e); } catch (Exception e) { log.warn("eIDAS AuthnRequest generation FAILED.", e); throw new TaskExecutionException(pendingReq, e.getMessage(), e); } } /** * Select a forward URL from configuration for a specific environment *

* Info: This method is needed, because eIDAS Ref. Impl only supports one countrycode on each instance. * In consequence, more than one eIDAS Ref. Impl nodes are required to support producation, testing, or QS stages * for one country by using one ms-specific eIDAS connector * * @param environment Environment selector from CountrySlection page * @return */ private String selectedForwardURLForEnvironment(String environment) { log.trace("Starting endpoint selection process for environment: " + environment + " ... "); if (environment.equalsIgnoreCase(MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_PRODUCTION)) return basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_URL); else if (environment.equalsIgnoreCase(MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_QS)) return basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_URL + "." + MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_QS); else if (environment.equalsIgnoreCase(MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_TESTING)) return basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_URL + "." + MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_TESTING); else if (environment.equalsIgnoreCase(MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_DEVELOPMENT)) return basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_URL + "." + MSeIDASNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_DEVELOPMENT); log.info("Environment selector: " + environment + " is not supported"); return null; } private ImmutableAttributeMap translateToEidasAttributes(final Map requiredAttributes) { ImmutableAttributeMap.Builder builder = ImmutableAttributeMap.builder(); for (Map.Entry attribute : requiredAttributes.entrySet()) { final String name = attribute.getKey(); final ImmutableSortedSet> byFriendlyName = attrRegistry.getCoreAttributeRegistry().getByFriendlyName(name); if (!byFriendlyName.isEmpty()) { final AttributeDefinition attributeDefinition = byFriendlyName.first(); builder.put(AttributeDefinition.builder(attributeDefinition).required(attribute.getValue()).build()); } else log.warn("Can NOT request UNKNOWN attribute: " + attribute.getKey() + " Ignore it!"); } return builder.build(); } private BinaryLightToken putRequestInCommunicationCache(ILightRequest iLightRequest) throws ServletException { final BinaryLightToken binaryLightToken; try { final SpecificConnectorCommunicationServiceImpl springManagedSpecificConnectorCommunicationService = (SpecificConnectorCommunicationServiceImpl) context.getBean(SpecificCommunicationDefinitionBeanNames.SPECIFIC_CONNECTOR_COMMUNICATION_SERVICE.toString()); binaryLightToken = springManagedSpecificConnectorCommunicationService.putRequest(iLightRequest); } catch (SpecificCommunicationException e) { log.error("Unable to process specific request"); throw new ServletException(e); } return binaryLightToken; } }