/* * 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: * * Output: * * Transitions: * * * @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 processedMsg = validateAssertion((PvpSProfileResponse) inboundMessage); if (processedMsg.getSecond()) { // forward to next matching step in case of ID Autria authentication was stopped by user executionContext.put(Constants.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 extractEntriesByBpk(Stream 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 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 null. * * @param samlResp SAML2 response * @return Sub-StatusCode or null 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 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 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(); } }