/* * Copyright 2018 A-SIT Plus GmbH * AT-specific eIDAS Connector has been developed in a cooperation between EGIZ, * A-SIT Plus GmbH, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "License"); * You may not use this work except in compliance with the License. * You may obtain a copy of the License at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * 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. * * 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 at.asitplus.eidas.specific.modules.auth.eidas.v2.tasks; import java.io.IOException; import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; import at.asitplus.eidas.specific.core.MsConnectorEventCodes; import at.asitplus.eidas.specific.core.MsEidasNodeConstants; import at.asitplus.eidas.specific.core.gui.StaticGuiBuilderConfiguration; import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.EidPreProcessingException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.EidasSAuthenticationException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.service.ICcSpecificEidProcessingService; import at.asitplus.eidas.specific.modules.core.eidas.EidasConstants; import at.gv.egiz.eaaf.core.api.gui.ISpringMvcGuiFormBuilder; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; 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.EaafException; import at.gv.egiz.eaaf.core.exceptions.GuiBuildException; 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.light.ILightRequest; import eu.eidas.auth.commons.light.impl.LightRequest; 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.SpecificCommunicationService; import lombok.extern.slf4j.Slf4j; /** * Generates the authn request to the eIDAS Node. This is the first task in the process. * Input: * * Output: * * Transitions: * * * @author tlenz * @author ckollmann */ @Slf4j @Component("GenerateAuthnRequestTask") public class GenerateAuthnRequestTask extends AbstractAuthServletTask { @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired IConfiguration basicConfig; @Autowired ApplicationContext context; @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired ITransactionStorage transactionStore; @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired ISpringMvcGuiFormBuilder guiBuilder; @Autowired ICcSpecificEidProcessingService ccSpecificProcessing; @Override public void execute(ExecutionContext executionContext, HttpServletRequest request, HttpServletResponse response) throws TaskExecutionException { try { final String citizenCountryCode = extractCitizenCountryCode(executionContext); final String environment = (String) executionContext.get(MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT); final String issuer = loadIssuerFromConfig(); // inject MS-Connector staging parameters injectStagingWorkaroundForMsConnector(); final LightRequest lightAuthnReq = buildEidasAuthnRequest(citizenCountryCode, issuer); // store pending request after possible updates requestStoreage.storePendingRequest(pendingReq); final BinaryLightToken token = putRequestInCommunicationCache(lightAuthnReq); final String tokenBase64 = BinaryLightTokenHelper.encodeBinaryLightTokenBase64(token); workaroundRelayState(lightAuthnReq); final String forwardUrl = selectForwardUrl(environment); String configValue = basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_NODE_FORWARD_METHOD, Constants.FORWARD_METHOD_GET); boolean useHttpRedirect = configValue.equals(Constants.FORWARD_METHOD_GET); if (useHttpRedirect) { sendRedirect(response, tokenBase64, forwardUrl); } else { sendPost(request, response, tokenBase64, forwardUrl); } revisionsLogger.logEvent(pendingReq, MsConnectorEventCodes.EIDAS_NODE_CONNECTED, lightAuthnReq.getId()); log.info("Allowed LoA: {}", StringUtils.join(pendingReq.getServiceProviderConfiguration().getRequiredLoA(),", ")); } catch (final EidasSAuthenticationException e) { throw new TaskExecutionException(pendingReq, "eIDAS AuthnRequest generation FAILED.", e); } catch (final Exception e) { log.warn("eIDAS AuthnRequest generation FAILED.", e); throw new TaskExecutionException(pendingReq, e.getMessage(), e); } } @NotNull private String extractCitizenCountryCode(ExecutionContext executionContext) throws EidasSAuthenticationException { final String result = (String) executionContext.get(MsEidasNodeConstants.REQ_PARAM_SELECTED_COUNTRY); // illegal state; task should not have been executed without a selected country if (StringUtils.isEmpty(result)) { throw new EidasSAuthenticationException("eidas.03", new Object[]{""}); } // TODO: maybe add countryCode validation before request ref. impl. eIDAS node log.info("Request eIDAS auth. for citizen of country: {}", result); revisionsLogger.logEvent(pendingReq, MsConnectorEventCodes.COUNTRY_SELECTED, result); return result; } @NotNull private String loadIssuerFromConfig() throws EaafConfigurationException { final String result = basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_ENTITYID); if (StringUtils.isEmpty(result)) { 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}); } return result; } @NotNull private LightRequest buildEidasAuthnRequest(String citizenCountryCode, String issuer) throws EidPreProcessingException { final LightRequest.Builder builder = LightRequest.builder(); builder.id(UUID.randomUUID().toString()); // set nameIDFormat builder.nameIdFormat( authConfig.getBasicConfiguration(Constants.CONFIG_PROP_EIDAS_NODE_NAMEIDFORMAT)); builder.citizenCountryCode(citizenCountryCode); builder.issuer(issuer); // Add country-specific information into eIDAS request ccSpecificProcessing.preProcess(citizenCountryCode, pendingReq, builder); return builder.build(); } private BinaryLightToken putRequestInCommunicationCache(ILightRequest lightRequest) throws ServletException { final BinaryLightToken binaryLightToken; try { String beanName = SpecificCommunicationDefinitionBeanNames.SPECIFIC_CONNECTOR_COMMUNICATION_SERVICE.toString(); final SpecificCommunicationService service = (SpecificCommunicationService) context.getBean(beanName); binaryLightToken = service.putRequest(lightRequest); } catch (final SpecificCommunicationException e) { log.error("Unable to process specific request"); throw new ServletException(e); } return binaryLightToken; } /** * Workaround, because eIDAS node ref. impl. does not return relayState */ private void workaroundRelayState(LightRequest lightAuthnReq) throws EaafException { if (basicConfig.getBasicConfigurationBoolean( 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); } } @NotNull private String selectForwardUrl(String environment) throws EaafConfigurationException { String result = basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL); if (StringUtils.isNotEmpty(environment)) { result = selectedForwardUrlForEnvironment(environment); } if (StringUtils.isEmpty(result)) { log.warn("NO ForwardURL defined in configuration. Can NOT forward to eIDAS node! Process stops"); throw new EaafConfigurationException("config.08", new Object[]{ environment == null ? Constants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL : Constants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL + "." + environment }); } log.debug("ForwardURL: {} selected to forward eIDAS request", result); return result; } private void injectStagingWorkaroundForMsConnector() throws EaafException { String alternativReturnEndpoint = basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_WORKAROUND_STAGING_MS_CONNECTOR); if (StringUtils.isNotEmpty(alternativReturnEndpoint)) { log.info("Inject alternative MS-Connector end-point: {}", alternativReturnEndpoint); pendingReq.setRawDataToTransaction( MsEidasNodeConstants.EXECCONTEXT_PARAM_MSCONNECTOR_STAGING, alternativReturnEndpoint); } } /** * 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 production, testing, or QS stages for one * country by using one ms-specific eIDAS connector * * @param environment Environment selector from CountrySlection page * @return the URL from the configuration */ 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(EidasConstants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL); } else if (environment.equalsIgnoreCase(MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_QS)) { return basicConfig.getBasicConfiguration(EidasConstants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL + "." + MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_QS); } else if (environment.equalsIgnoreCase( MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_TESTING)) { return basicConfig.getBasicConfiguration(EidasConstants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL + "." + MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_TESTING); } else if (environment.equalsIgnoreCase( MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_DEVELOPMENT)) { return basicConfig.getBasicConfiguration(EidasConstants.CONIG_PROPS_EIDAS_CONNECTOR_NODE_FORWARD_URL + "." + MsEidasNodeConstants.REQ_PARAM_SELECTED_ENVIRONMENT_VALUE_DEVELOPMENT); } log.info("Environment selector: {} is not supported", environment); return null; } private void sendRedirect(HttpServletResponse response, String tokenBase64, String forwardUrl) throws IOException { log.debug("Use http-redirect for eIDAS node forwarding ... "); final UriComponentsBuilder redirectUrl = UriComponentsBuilder.fromHttpUrl(forwardUrl); redirectUrl.queryParam(EidasParameterKeys.TOKEN.toString(), tokenBase64); response.sendRedirect(redirectUrl.build().encode().toString()); } private void sendPost(HttpServletRequest request, HttpServletResponse response, String tokenBase64, String forwardUrl) throws GuiBuildException { log.debug("Use http-post for eIDAS node forwarding ... "); final StaticGuiBuilderConfiguration config = new StaticGuiBuilderConfiguration( basicConfig, pendingReq, EidasConstants.TEMPLATE_POST_FORWARD_NAME, null, resourceLoader); config.putCustomParameter(null, EidasConstants.TEMPLATE_POST_FORWARD_ENDPOINT, forwardUrl); String token = EidasParameterKeys.TOKEN.toString(); config.putCustomParameter(null, EidasConstants.TEMPLATE_POST_FORWARD_TOKEN_NAME, token); config.putCustomParameter(null, EidasConstants.TEMPLATE_POST_FORWARD_TOKEN_VALUE, tokenBase64); guiBuilder.build(request, response, config, "Forward to eIDASNode form"); } }