package at.asitplus.eidas.specific.modules.auth.idaustria.tasks; import java.io.IOException; import java.util.Set; import javax.naming.ConfigurationException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.transform.TransformerException; import org.apache.commons.lang3.StringUtils; 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.beans.factory.annotation.Autowired; import at.asitplus.eidas.specific.modules.auth.idaustria.IdAustriaAuthConstants; import at.asitplus.eidas.specific.modules.auth.idaustria.utils.IdAustriaAuthCredentialProvider; import at.asitplus.eidas.specific.modules.auth.idaustria.utils.IdAustriaAuthMetadataProvider; import at.asitplus.eidas.specific.modules.auth.idaustria.utils.Utils; 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.EaafAuthenticationException; import at.gv.egiz.eaaf.core.exceptions.EaafBuilderException; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; 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; /** * ID Austria authentication task that receives the SAML2 response from ID * Austria system. * * @author tlenz * */ @Slf4j public class ReceiveFromIdAustriaSystemTask extends AbstractAuthServletTask { 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 federated IDP"; private static final String ERROR_MSG_01 = "Processing PVP response from 'ms-specific eIDAS node' FAILED."; private static final String ERROR_MSG_02 = "PVP response decrytion FAILED. No credential found."; private static final String ERROR_MSG_03 = "PVP response validation FAILED."; @Autowired private SamlVerificationEngine samlVerificationEngine; @Autowired private IdAustriaAuthCredentialProvider credentialProvider; @Autowired(required = true) IdAustriaAuthMetadataProvider metadataProvider; /* * (non-Javadoc) * * @see * at.gv.egovernment.moa.id.auth.modules.AbstractAuthServletTask#execute(at.gv. * egovernment.moa.id.process.api.ExecutionContext, * javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ @Override public void execute(ExecutionContext executionContext, HttpServletRequest request, HttpServletResponse response) throws TaskExecutionException { InboundMessage msg = null; try { IDecoder decoder = null; EaafUriCompare comperator = null; // select Response Binding if (request.getMethod().equalsIgnoreCase("POST")) { decoder = new PostBinding(); comperator = new EaafUriCompare(pendingReq.getAuthUrl() + IdAustriaAuthConstants.ENDPOINT_POST); log.trace("Receive PVP Response from 'ID Austria', by using POST-Binding."); } else if (request.getMethod().equalsIgnoreCase("GET")) { decoder = new RedirectBinding(); comperator = new EaafUriCompare(pendingReq.getAuthUrl() + IdAustriaAuthConstants.ENDPOINT_REDIRECT); log.trace("Receive PVP Response from 'ID Austria', by using Redirect-Binding."); } else { log.warn("Receive PVP Response, but Binding (" + request.getMethod() + ") is not supported."); throw new AuthnResponseValidationException(ERROR_PVP_03, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING }); } // decode PVP response object msg = (InboundMessage) decoder.decode( request, response, metadataProvider, IDPSSODescriptor.DEFAULT_ELEMENT_NAME, comperator); // validate response signature if (!msg.isVerified()) { samlVerificationEngine.verify(msg, TrustEngineFactory.getSignatureKnownKeysTrustEngine(metadataProvider)); msg.setVerified(true); } // validate assertion final Pair processedMsg = preProcessAuthResponse((PvpSProfileResponse) msg); // check if SAML2 response contains user-stop decision if (processedMsg.getSecond()) { stopProcessFromUserDecision(executionContext, request, response); } else { // validate entityId of response final String idAustriaEntityID = Utils.getIdAustriaEntityId(pendingReq.getServiceProviderConfiguration(), authConfig); final String respEntityId = msg.getEntityID(); if (!idAustriaEntityID.equals(respEntityId)) { log.warn("Response Issuer is not a 'ID Austria System'. Stopping eIDAS authentication ..."); throw new AuthnResponseValidationException(ERROR_PVP_08, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING, msg.getEntityID() }); } // initialize Attribute extractor final AssertionAttributeExtractor extractor = new AssertionAttributeExtractor(processedMsg.getFirst().getResponse()); getAuthDataFromInterfederation(extractor); // set NeedConsent to false, because user gives consont during authentication pendingReq.setNeedUserConsent(false); // store pending-request requestStoreage.storePendingRequest(pendingReq); // write log entries // revisionsLogger.logEvent(pendingReq, // EidasAuthEventConstants.AUTHPROCESS_EIDAS_AT_CONNECTOR_MDS_VALID); log.info("Receive a valid assertion from IDP " + msg.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 'ID Austria System': {}", samlRequest, null, e); throw new TaskExecutionException(pendingReq, ERROR_MSG_00, new AuthnResponseValidationException(ERROR_PVP_11, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING }, e)); } catch (IOException | MarshallingException | TransformerException e) { log.debug("Processing PVP response from 'ID Austria System' FAILED.", e); throw new TaskExecutionException(pendingReq, ERROR_MSG_01, new AuthnResponseValidationException(ERROR_PVP_12, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING, e.getMessage() }, e)); } catch (final CredentialsNotAvailableException e) { log.debug("PVP response decrytion FAILED. No credential found.", e); throw new TaskExecutionException(pendingReq, ERROR_MSG_02, new AuthnResponseValidationException(ERROR_PVP_10, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING }, e)); } catch (final Exception e) { log.debug("PVP response validation FAILED. Msg:" + e.getMessage(), e); throw new TaskExecutionException(pendingReq, ERROR_MSG_03, new AuthnResponseValidationException(ERROR_PVP_12, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING, e.getMessage() }, e)); } } private void getAuthDataFromInterfederation(AssertionAttributeExtractor extractor) throws EaafBuilderException, ConfigurationException { final Set requiredEidasNodeAttributes = IdAustriaAuthConstants.DEFAULT_REQUIRED_PVP_ATTRIBUTE_NAMES; try { // check if all attributes are include if (!extractor.containsAllRequiredAttributes(requiredEidasNodeAttributes)) { log.warn("PVP Response from 'ID Austria System' contains not all requested attributes."); throw new AssertionValidationExeption(ERROR_PVP_06, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING }); } // copy attributes into MOASession final AuthProcessDataWrapper session = pendingReq.getSessionData( AuthProcessDataWrapper.class); // validate response attributes validateResponseAttributes(extractor); // inject all attributes into session final Set includedAttrNames = extractor.getAllIncludeAttributeNames(); for (final String attrName : includedAttrNames) { injectAuthInfosIntoSession(session, attrName, extractor.getSingleAttributeValue(attrName)); } // set foreigner flag session.setForeigner(false); // mark it as ID Austria process, because we have no baseId session.setEidProcess(true); // set IssuerInstant from Assertion session.setIssueInstant(extractor.getAssertionIssuingDate()); // set mandate flag session.setUseMandates(checkIfMandateInformationIsAvailable(extractor)); } catch (final EaafException | IOException e) { throw new EaafBuilderException(ERROR_PVP_06, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING }, e.getMessage(), e); } } /** * Check if mandate information is available. * * @param extractor Assertion from ID Austria system. * @return true if mandate was used, otherwise false */ private boolean checkIfMandateInformationIsAvailable(AssertionAttributeExtractor extractor) { boolean isMandateIncluded = extractor.containsAttribute(PvpAttributeDefinitions.MANDATE_TYPE_NAME); log.debug("Response from ID-Austria system contains {} mandate information. Switch to {} ... ", isMandateIncluded ? "" : "no", isMandateIncluded ? "mandate mode" : "normal mode"); return isMandateIncluded; } private void validateResponseAttributes(AssertionAttributeExtractor extractor) throws EaafAuthenticationException { final String bpkTarget = extractor.getSingleAttributeValue( PvpAttributeDefinitions.EID_SECTOR_FOR_IDENTIFIER_NAME); final String spTarget = pendingReq.getServiceProviderConfiguration().getAreaSpecificTargetIdentifier(); if (StringUtils.isNotEmpty(bpkTarget) && bpkTarget.equals(spTarget)) { log.debug("Find attr: {} that matches to requested bPK target. bPK should be valid", PvpAttributeDefinitions.EID_SECTOR_FOR_IDENTIFIER_FRIENDLY_NAME); } else { log.trace("Find not attr: {}. Validation bPK attribute ... ", PvpAttributeDefinitions.EID_SECTOR_FOR_IDENTIFIER_FRIENDLY_NAME); final String bpk = extractor.getSingleAttributeValue(PvpAttributeDefinitions.BPK_NAME); final String[] split = bpk.split(":", 2); if (split.length == 2) { if (!spTarget.endsWith(split[0])) { log.error("bPK from response: {} does not match to SP target: {}", bpk, spTarget); throw new EaafAuthenticationException(IdAustriaAuthConstants.ERRORTYPE_06, null); } else { log.trace("Prefix of PVP bPK attribte matches to SP configuration. bPK looks valid"); } } else { log.warn("Find suspect bPK that has no prefix. Use it as it is ... "); } } } private void injectAuthInfosIntoSession(AuthProcessDataWrapper session, String attrName, String attrValue) throws EaafStorageException, IOException { log.trace("Inject attribute: {} with value: {} into AuthSession", attrName, attrValue); log.debug("Inject attribute: {} into AuthSession", attrName); session.setGenericDataToSession(attrName, attrValue); } private Pair preProcessAuthResponse(PvpSProfileResponse msg) throws IOException, MarshallingException, TransformerException, CredentialsNotAvailableException, AuthnResponseValidationException, SamlAssertionValidationExeption { log.debug("Start PVP-2x assertion processing... "); final Response samlResp = (Response) msg.getResponse(); log.info("Receive ID Austria response with Id: {}", samlResp.getID()); // check SAML2 response status-code if (samlResp.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS)) { // validate PVP 2.1 assertion samlVerificationEngine.validateAssertion(samlResp, credentialProvider.getMessageEncryptionCredential(), pendingReq.getAuthUrl() + IdAustriaAuthConstants.ENDPOINT_METADATA, IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING); msg.setSamlMessage(Saml2Utils.asDomDocument(samlResp).getDocumentElement()); revisionsLogger.logEvent(pendingReq, IdAustriaAuthConstants.AUTHPROCESS_EIDAS_AT_PROXYSERVICE_RESPONSE_SUCCESS, samlResp.getID()); return Pair.newInstance(msg, false); } else { log.info("Receive StatusCode " + samlResp.getStatus().getStatusCode().getValue() + " from 'ms-specific eIDAS node'."); final StatusCode subStatusCode = getSubStatusCode(samlResp); if (subStatusCode != null && IdAustriaAuthConstants.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, IdAustriaAuthConstants.AUTHPROCESS_EIDAS_AT_PROXYSERVICE_RESPONSE_ERROR, samlResp.getID()); throw new AuthnResponseValidationException(ERROR_PVP_05, new Object[] { IdAustriaAuthConstants.MODULE_NAME_FOR_LOGGING, samlResp.getIssuer().getValue(), samlResp.getStatus().getStatusCode().getValue(), samlResp.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; } }