diff options
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.java | 379 |
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(); + + } + + +} |