/*
* 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:
*
* - {@link at.asitplus.eidas.specific.modules.auth.eidas.v2.tasks.ReceiveAuthnResponseTask}
* to read the response from the eIDAS Node
*
*
* @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());
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");
}
}