aboutsummaryrefslogtreecommitdiff
path: root/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java
diff options
context:
space:
mode:
Diffstat (limited to 'modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java')
-rw-r--r--modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java379
1 files changed, 379 insertions, 0 deletions
diff --git a/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java
new file mode 100644
index 00000000..514e38ba
--- /dev/null
+++ b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2021 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 static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.CONTEXT_FLAG_ADVANCED_MATCHING_FAILED;
+import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON;
+import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.TRANSITION_TO_GENERATE_OTHER_LOGIN_METHOD_GUI_TASK;
+import static at.asitplus.eidas.specific.modules.auth.eidas.v2.idaustriaclient.IdAustriaClientAuthConstants.MODULE_NAME_FOR_LOGGING;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.transform.TransformerException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
+import org.springframework.stereotype.Component;
+
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.MatchedPersonResult;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.RegisterResult;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleMobileSignatureData;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.WorkflowException;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.idaustriaclient.IdAustriaClientAuthConstants;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.idaustriaclient.IdAustriaClientAuthEventConstants;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.idaustriaclient.provider.IdAustriaClientAuthCredentialProvider;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.idaustriaclient.provider.IdAustriaClientAuthMetadataProvider;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.service.RegisterSearchService;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.service.RegisterSearchService.RegisterStatusResults;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.utils.MatchingTaskUtils;
+import at.gv.egiz.eaaf.core.api.data.PvpAttributeDefinitions;
+import at.gv.egiz.eaaf.core.api.idp.process.ExecutionContext;
+import at.gv.egiz.eaaf.core.exceptions.EaafBuilderException;
+import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException;
+import at.gv.egiz.eaaf.core.impl.data.Pair;
+import at.gv.egiz.eaaf.core.impl.idp.auth.modules.AbstractAuthServletTask;
+import at.gv.egiz.eaaf.modules.pvp2.api.binding.IDecoder;
+import at.gv.egiz.eaaf.modules.pvp2.exception.CredentialsNotAvailableException;
+import at.gv.egiz.eaaf.modules.pvp2.exception.SamlAssertionValidationExeption;
+import at.gv.egiz.eaaf.modules.pvp2.exception.SamlSigningException;
+import at.gv.egiz.eaaf.modules.pvp2.impl.binding.PostBinding;
+import at.gv.egiz.eaaf.modules.pvp2.impl.binding.RedirectBinding;
+import at.gv.egiz.eaaf.modules.pvp2.impl.message.InboundMessage;
+import at.gv.egiz.eaaf.modules.pvp2.impl.message.PvpSProfileResponse;
+import at.gv.egiz.eaaf.modules.pvp2.impl.utils.Saml2Utils;
+import at.gv.egiz.eaaf.modules.pvp2.impl.validation.EaafUriCompare;
+import at.gv.egiz.eaaf.modules.pvp2.impl.validation.TrustEngineFactory;
+import at.gv.egiz.eaaf.modules.pvp2.impl.verification.SamlVerificationEngine;
+import at.gv.egiz.eaaf.modules.pvp2.sp.exception.AssertionValidationExeption;
+import at.gv.egiz.eaaf.modules.pvp2.sp.exception.AuthnResponseValidationException;
+import at.gv.egiz.eaaf.modules.pvp2.sp.impl.utils.AssertionAttributeExtractor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Task that receives the SAML2 response from ID Austria system.
+ * This corresponds to Step 15 in the eIDAS Matching Concept.
+ * Input:
+ * <ul>
+ * <li>{@link Constants#DATA_SIMPLE_EIDAS} initial login data from user</li>
+ * <li>{@link Constants#DATA_INTERMEDIATE_RESULT} results from search in registers with personIdentifier</li>
+ * </ul>
+ * Output:
+ * <ul>
+ * <li>{@link Constants#DATA_PERSON_MATCH_RESULT} if one register result found</li>
+ * </ul>
+ * Transitions:
+ * <ul>
+ * <li>{@link GenerateAustrianResidenceGuiTask} if no results in registers were found</li>
+ * <li>{@link CreateIdentityLinkTask} if one exact match between initial register search (with MDS) data and
+ * register search with MPS data exists</li>
+ * <li>{@link GenerateOtherLoginMethodGuiTask} if a user input error has happened</li>
+ * </ul>
+ *
+ * @author tlenz
+ * @author ckollmann
+ */
+@Slf4j
+@Component("ReceiveMobilePhoneSignatureResponseTask")
+public class ReceiveMobilePhoneSignatureResponseTask extends AbstractAuthServletTask {
+
+ private final SamlVerificationEngine samlVerificationEngine;
+ private final RegisterSearchService registerSearchService;
+ private final IdAustriaClientAuthCredentialProvider credentialProvider;
+ private final IdAustriaClientAuthMetadataProvider metadataProvider;
+
+ private static final String ERROR_PVP_03 = "sp.pvp2.03";
+ private static final String ERROR_PVP_05 = "sp.pvp2.05";
+ private static final String ERROR_PVP_06 = "sp.pvp2.06";
+ private static final String ERROR_PVP_08 = "sp.pvp2.08";
+ private static final String ERROR_PVP_10 = "sp.pvp2.10";
+ private static final String ERROR_PVP_11 = "sp.pvp2.11";
+ private static final String ERROR_PVP_12 = "sp.pvp2.12";
+
+ private static final String ERROR_MSG_00 = "Receive INVALID PVP Response from ID Austria system";
+ private static final String ERROR_MSG_01 = "Processing PVP response from 'ID Austria system' FAILED.";
+ private static final String ERROR_MSG_02 = "PVP response decryption FAILED. No credential found.";
+ private static final String ERROR_MSG_03 = "PVP response validation FAILED.";
+
+ private static final String MSG_PROP_23 = "module.eidasauth.matching.23";
+ private static final String MSG_PROP_24 = "module.eidasauth.matching.24";
+
+ /**
+ * Creates the new task, with autowired dependencies from Spring.
+ */
+ @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+ public ReceiveMobilePhoneSignatureResponseTask(SamlVerificationEngine samlVerificationEngine,
+ RegisterSearchService registerSearchService,
+ IdAustriaClientAuthCredentialProvider credentialProvider,
+ IdAustriaClientAuthMetadataProvider metadataProvider) {
+ this.samlVerificationEngine = samlVerificationEngine;
+ this.registerSearchService = registerSearchService;
+ this.credentialProvider = credentialProvider;
+ this.metadataProvider = metadataProvider;
+ }
+
+ @Override
+ public void execute(ExecutionContext executionContext, HttpServletRequest request, HttpServletResponse response)
+ throws TaskExecutionException {
+ try {
+ log.trace("Starting ReceiveMobilePhoneSignatureResponseTask");
+ IDecoder decoder = loadDecoder(request);
+ EaafUriCompare comparator = loadComparator(request);
+ InboundMessage inboundMessage = decodeAndVerifyMessage(request, response, decoder, comparator);
+ Pair<PvpSProfileResponse, Boolean> processedMsg = validateAssertion((PvpSProfileResponse) inboundMessage);
+ if (processedMsg.getSecond()) {
+ // forward to next matching step in case of ID Autria authentication was stopped by user
+ executionContext.put(TRANSITION_TO_GENERATE_OTHER_LOGIN_METHOD_GUI_TASK, true);
+ executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON, MSG_PROP_23);
+ executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED, true);
+ return;
+
+ }
+
+ // validate SAML2 response
+ validateEntityId(inboundMessage);
+ log.info("Receive a valid assertion from IDP " + inboundMessage.getEntityID());
+
+ // load already existing information from session
+ SimpleEidasData eidasData = MatchingTaskUtils.getInitialEidasData(pendingReq);
+ RegisterStatusResults initialSearchResult = MatchingTaskUtils.getIntermediateMatchingResult(pendingReq);
+
+ // extract user information from ID Austria authentication
+ AssertionAttributeExtractor extractor = new AssertionAttributeExtractor(processedMsg.getFirst().getResponse());
+ SimpleMobileSignatureData simpleMobileSignatureData = getAuthDataFromInterfederation(extractor);
+
+ // check if MDS from ID Austria authentication matchs to eIDAS authentication
+ if (!simpleMobileSignatureData.equalsSimpleEidasData(eidasData)) {
+ executionContext.put(TRANSITION_TO_GENERATE_OTHER_LOGIN_METHOD_GUI_TASK, true);
+ executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON, MSG_PROP_24);
+ executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED, true);
+ return;
+
+ }
+
+ // search entry in initial search result from steps before and build new RegisterSearchResult
+ RegisterStatusResults registerResult = new RegisterStatusResults(initialSearchResult.getOperationStatus(),
+ extractEntriesByBpk(initialSearchResult.getResultsZmr().stream(), simpleMobileSignatureData.getBpk()),
+ extractEntriesByBpk(initialSearchResult.getResultsErnp().stream(), simpleMobileSignatureData.getBpk()));
+
+ if (registerResult.getResultCount() != 1) {
+ throw new WorkflowException("matchWithIDAustriaAuthentication",
+ "Suspect state detected. MDS matches to eIDAS authentication "
+ + "but register search-result with MDS contains #" + registerResult.getResultCount()
+ + " entry with bPK from ID Austria authentication", false);
+
+ } else {
+ // perform kit operation
+ registerSearchService.step7aKittProcess(registerResult, eidasData);
+
+ // store search result to re-used in CreateIdentityLink step, because there we need bPK and MDS
+ MatchingTaskUtils.storeFinalMatchingResult(pendingReq,
+ MatchedPersonResult.generateFormMatchingResult(registerResult.getResult(),
+ eidasData.getCitizenCountryCode()));
+
+ }
+
+ } catch (final AuthnResponseValidationException e) {
+ throw new TaskExecutionException(pendingReq, ERROR_MSG_03, e);
+
+ } catch (MessageDecodingException | SecurityException | SamlSigningException e) {
+ //final String samlRequest = request.getParameter("SAMLRequest");
+ //log.debug("Receive INVALID PVP Response from 'ms-specific eIDAS node': {}",
+ // samlRequest, null, e);
+ throw new TaskExecutionException(pendingReq, ERROR_MSG_00,
+ new AuthnResponseValidationException(ERROR_PVP_11, new Object[]{MODULE_NAME_FOR_LOGGING}, e));
+
+ } catch (IOException | MarshallingException | TransformerException e) {
+ log.debug("Processing PVP response from 'ms-specific eIDAS node' FAILED.", e);
+ throw new TaskExecutionException(pendingReq, ERROR_MSG_01,
+ new AuthnResponseValidationException(ERROR_PVP_12, new Object[]{MODULE_NAME_FOR_LOGGING, e.getMessage()}, e));
+
+ } catch (final CredentialsNotAvailableException e) {
+ log.debug("PVP response decryption FAILED. No credential found.", e);
+ throw new TaskExecutionException(pendingReq, ERROR_MSG_02,
+ new AuthnResponseValidationException(ERROR_PVP_10, new Object[]{MODULE_NAME_FOR_LOGGING}, e));
+
+ } catch (final Exception e) {
+ // todo catch ManualFixNecessaryException in any other way?
+ log.debug("PVP response validation FAILED. Msg:" + e.getMessage(), e);
+ throw new TaskExecutionException(pendingReq, ERROR_MSG_03,
+ new AuthnResponseValidationException(ERROR_PVP_12, new Object[]{MODULE_NAME_FOR_LOGGING, e.getMessage()}, e));
+
+ }
+ }
+
+ private List<RegisterResult> extractEntriesByBpk(Stream<RegisterResult> stream, String bpk) {
+ return stream.filter(el -> bpk.equals(el.getBpk())).collect(Collectors.toList());
+
+ }
+
+ @NotNull
+ private InboundMessage decodeAndVerifyMessage(HttpServletRequest request, HttpServletResponse response,
+ IDecoder decoder, EaafUriCompare comparator) throws Exception {
+ InboundMessage inboundMessage = (InboundMessage) decoder.decode(request, response, metadataProvider,
+ IDPSSODescriptor.DEFAULT_ELEMENT_NAME, comparator);
+ if (!inboundMessage.isVerified()) {
+ samlVerificationEngine.verify(inboundMessage, TrustEngineFactory.getSignatureKnownKeysTrustEngine(
+ metadataProvider));
+ inboundMessage.setVerified(true);
+ }
+ return inboundMessage;
+ }
+
+ private void validateEntityId(InboundMessage inboundMessage) throws AuthnResponseValidationException {
+ final String msNodeEntityID = authConfig
+ .getBasicConfiguration(IdAustriaClientAuthConstants.CONFIG_PROPS_ID_AUSTRIA_ENTITYID);
+ final String respEntityId = inboundMessage.getEntityID();
+ if (!msNodeEntityID.equals(respEntityId)) {
+ log.warn("Response Issuer is not from valid 'ID Austria IDP'. Stopping ID Austria authentication ...");
+ throw new AuthnResponseValidationException(ERROR_PVP_08,
+ new Object[]{MODULE_NAME_FOR_LOGGING,
+ inboundMessage.getEntityID()});
+ }
+ }
+
+ @NotNull
+ private EaafUriCompare loadComparator(HttpServletRequest request) throws AuthnResponseValidationException {
+ if (request.getMethod().equalsIgnoreCase("POST")) {
+ log.trace("Receive PVP Response from 'ID Austria system', by using POST-Binding.");
+ return new EaafUriCompare(pendingReq.getAuthUrl() + IdAustriaClientAuthConstants.ENDPOINT_POST);
+ } else if (request.getMethod().equalsIgnoreCase("GET")) {
+ log.trace("Receive PVP Response from 'ID Austria system', by using Redirect-Binding.");
+ return new EaafUriCompare(pendingReq.getAuthUrl() + IdAustriaClientAuthConstants.ENDPOINT_REDIRECT);
+ } else {
+ log.warn("Receive PVP Response from 'ID Austria system', but Binding {} is not supported.", request.getMethod());
+ throw new AuthnResponseValidationException(ERROR_PVP_03, new Object[]{MODULE_NAME_FOR_LOGGING});
+ }
+ }
+
+ @NotNull
+ private IDecoder loadDecoder(HttpServletRequest request) throws AuthnResponseValidationException {
+ if (request.getMethod().equalsIgnoreCase("POST")) {
+ log.trace("Receive PVP Response from 'ID Austria system', by using POST-Binding.");
+ return new PostBinding();
+ } else if (request.getMethod().equalsIgnoreCase("GET")) {
+ log.trace("Receive PVP Response from 'ID Austria system', by using Redirect-Binding.");
+ return new RedirectBinding();
+ } else {
+ log.warn("Receive PVP Response from 'ID Austria system', but Binding {} is not supported.", request.getMethod());
+ throw new AuthnResponseValidationException(ERROR_PVP_03, new Object[]{MODULE_NAME_FOR_LOGGING});
+ }
+ }
+
+ private Pair<PvpSProfileResponse, Boolean> validateAssertion(PvpSProfileResponse msg)
+ throws IOException, MarshallingException, TransformerException,
+ CredentialsNotAvailableException, AuthnResponseValidationException, SamlAssertionValidationExeption {
+ log.debug("Start PVP21 assertion processing... ");
+ final Response response = (Response) msg.getResponse();
+ if (response.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS)) {
+ samlVerificationEngine.validateAssertion(response,
+ credentialProvider.getMessageEncryptionCredential(),
+ pendingReq.getAuthUrl() + IdAustriaClientAuthConstants.ENDPOINT_METADATA,
+ MODULE_NAME_FOR_LOGGING);
+ msg.setSamlMessage(Saml2Utils.asDomDocument(response).getDocumentElement());
+ revisionsLogger.logEvent(pendingReq,
+ IdAustriaClientAuthEventConstants.AUTHPROCESS_ID_AUSTRIA_RESPONSE_RECEIVED,
+ response.getID());
+ return Pair.newInstance(msg, false);
+ } else {
+ log.info("Receive StatusCode {} from 'ms-specific eIDAS node'.", response.getStatus().getStatusCode().getValue());
+ StatusCode subStatusCode = getSubStatusCode(response);
+ if (subStatusCode != null
+ && IdAustriaClientAuthConstants.SAML2_STATUSCODE_USERSTOP.equals(subStatusCode.getValue())) {
+ log.info("Find 'User-Stop operation' in SAML2 response. Stopping authentication process ... ");
+ return Pair.newInstance(msg, true);
+ }
+
+ revisionsLogger.logEvent(pendingReq,
+ IdAustriaClientAuthEventConstants.AUTHPROCESS_ID_AUSTRIA_RESPONSE_RECEIVED_ERROR);
+ throw new AuthnResponseValidationException(ERROR_PVP_05,
+ new Object[]{MODULE_NAME_FOR_LOGGING,
+ response.getIssuer().getValue(),
+ response.getStatus().getStatusCode().getValue(),
+ response.getStatus().getStatusMessage().getValue()});
+ }
+ }
+
+ /**
+ * Get SAML2 Sub-StatusCode if not <code>null</code>.
+ *
+ * @param samlResp SAML2 response
+ * @return Sub-StatusCode or <code>null</code> if it's not set
+ */
+ private StatusCode getSubStatusCode(Response samlResp) {
+ if (samlResp.getStatus().getStatusCode().getStatusCode() != null
+ && StringUtils.isNotEmpty(samlResp.getStatus().getStatusCode().getStatusCode().getValue())) {
+ return samlResp.getStatus().getStatusCode().getStatusCode();
+ }
+ return null;
+ }
+
+ private SimpleMobileSignatureData getAuthDataFromInterfederation(AssertionAttributeExtractor extractor)
+ throws EaafBuilderException {
+ List<String> requiredAttributes = IdAustriaClientAuthConstants.DEFAULT_REQUIRED_PVP_ATTRIBUTE_NAMES;
+ SimpleMobileSignatureData.SimpleMobileSignatureDataBuilder builder = SimpleMobileSignatureData.builder();
+ if (!extractor.containsAllRequiredAttributes(requiredAttributes)) {
+ log.warn("PVP Response from 'ID Austria node' contains not all requested attributes.");
+ AssertionValidationExeption e = new AssertionValidationExeption(ERROR_PVP_06,
+ new Object[]{MODULE_NAME_FOR_LOGGING});
+ throw new EaafBuilderException(ERROR_PVP_06, null, e.getMessage(), e);
+ }
+ final Set<String> includedAttrNames = extractor.getAllIncludeAttributeNames();
+ for (final String attrName : includedAttrNames) {
+ if (PvpAttributeDefinitions.BPK_NAME.equals(attrName)) {
+ builder.bpk(extractor.getSingleAttributeValue(attrName));
+ }
+ if (PvpAttributeDefinitions.GIVEN_NAME_NAME.equals(attrName)) {
+ builder.givenName(extractor.getSingleAttributeValue(attrName));
+ }
+ if (PvpAttributeDefinitions.PRINCIPAL_NAME_NAME.equals(attrName)) {
+ builder.familyName(extractor.getSingleAttributeValue(attrName));
+ }
+ if (PvpAttributeDefinitions.BIRTHDATE_NAME.equals(attrName)) {
+ builder.dateOfBirth(extractor.getSingleAttributeValue(attrName));
+ }
+ if (PvpAttributeDefinitions.EID_CITIZEN_EIDAS_QAA_LEVEL_NAME.equals(attrName)) {
+ MatchingTaskUtils.getAuthProcessDataWrapper(pendingReq).setQaaLevel(
+ extractor.getSingleAttributeValue(attrName));
+ }
+ }
+ MatchingTaskUtils.getAuthProcessDataWrapper(pendingReq).setIssueInstant(extractor.getAssertionIssuingDate());
+ return builder.build();
+
+ }
+
+
+}