diff options
Diffstat (limited to 'eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java')
-rw-r--r-- | eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java new file mode 100644 index 00000000..0f40b337 --- /dev/null +++ b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/tasks/ReceiveMobilePhoneSignatureResponseTask.java @@ -0,0 +1,340 @@ +/* + * 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 at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; +import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.MergedRegisterSearchResult; +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.InvalidUserInputException; +import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.ManualFixNecessaryException; +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.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.data.AuthProcessDataWrapper; +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; +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 javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.transform.TransformerException; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.DATA_INITIAL_REGISTER_RESULT; +import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.DATA_SIMPLE_EIDAS; +import static at.asitplus.eidas.specific.modules.auth.eidas.v2.idaustriaclient.IdAustriaClientAuthConstants.MODULE_NAME_FOR_LOGGING; + +/** + * Task that receives the SAML2 response from ID Austria system. + * This corresponds to Step 15 in the eIDAS Matching Concept. + * + * @author tlenz + */ +@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."; + + /** + * Creates the new task, with autowired dependencies from Spring. + */ + 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()) { + stopProcessFromUserDecision(executionContext, request, response); + return; + } + + validateEntityId(inboundMessage); + AssertionAttributeExtractor extractor = new AssertionAttributeExtractor(processedMsg.getFirst().getResponse()); + + /* + * SAML2 response ist bereits vollständig validiert und die Attribute können aus dem + * <AssertionAttributeExtractor extractor> ausgelesen werden. + * Die AttributeNamen sind entsprechend PVP Spezifikation, z.B. PvpAttributeDefinitions.GIVEN_NAME_NAME + * + * --------------------------------------------------------------------------------------------- + * + * TODO: ab hier müssen wir wohl was anpassen + * + */ + + AuthProcessDataWrapper authProcessData = pendingReq.getSessionData(AuthProcessDataWrapper.class); + MergedRegisterSearchResult initialSearchResult = + authProcessData.getGenericDataFromSession(DATA_INITIAL_REGISTER_RESULT, MergedRegisterSearchResult.class); + SimpleEidasData eidasData = authProcessData.getGenericDataFromSession(DATA_SIMPLE_EIDAS, SimpleEidasData.class); + String bpkZp = extractBpkZp(extractor, authProcessData, eidasData); + + MergedRegisterSearchResult result = registerSearchService.searchWithBpkZp(bpkZp); + if (result.getResultCount() == 0) { + executionContext.put(Constants.TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK, true); + return; + } else if (result.getResultCount() == 1) { + String bpk = registerSearchService.step7aKittProcess(initialSearchResult, result, eidasData, pendingReq); + authProcessData.setGenericDataToSession(Constants.DATA_RESULT_MATCHING_BPK, bpk); + return; + } else if (result.getResultCount() > 1) { + throw new ManualFixNecessaryException("bpkZp: " + bpkZp); + } + + // set NeedConsent to false, because user gives consent during authentication + pendingReq.setNeedUserConsent(false); + log.info("Receive a valid assertion from IDP " + inboundMessage.getEntityID()); + } 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) { + e.printStackTrace(); + 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 String extractBpkZp(AssertionAttributeExtractor extractor, + AuthProcessDataWrapper authProcessData, + SimpleEidasData eidasData) throws EaafBuilderException, InvalidUserInputException { + SimpleMobileSignatureData simpleMobileSignatureData = getAuthDataFromInterfederation(extractor, authProcessData); + if (!simpleMobileSignatureData.equalsSimpleEidasData(eidasData)) { + //TODO User has cheated? + throw new InvalidUserInputException(); + } + return simpleMobileSignatureData.getBpk(); + } + + @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().getMessage()}); + } + } + + /** + * 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, + AuthProcessDataWrapper authProcessData) + throws EaafBuilderException { + List<String> requiredAttributes = IdAustriaClientAuthConstants.DEFAULT_REQUIRED_PVP_ATTRIBUTE_NAMES; + SimpleMobileSignatureData result = new SimpleMobileSignatureData(); + 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)) { + result.setBpk(extractor.getSingleAttributeValue(attrName)); + } + if (PvpAttributeDefinitions.GIVEN_NAME_NAME.equals(attrName)) { + result.setGivenName(extractor.getSingleAttributeValue(attrName)); + } + if (PvpAttributeDefinitions.PRINCIPAL_NAME_NAME.equals(attrName)) { + result.setFamilyName(extractor.getSingleAttributeValue(attrName)); + } + if (PvpAttributeDefinitions.BIRTHDATE_NAME.equals(attrName)) { + result.setDateOfBirth(extractor.getSingleAttributeValue(attrName)); + } + if (PvpAttributeDefinitions.EID_CITIZEN_EIDAS_QAA_LEVEL_NAME.equals(attrName)) { + authProcessData.setQaaLevel(extractor.getSingleAttributeValue(attrName)); + } + } + authProcessData.setIssueInstant(extractor.getAssertionIssuingDate()); + return result; + } + + +} |