diff options
Diffstat (limited to 'modules/authmodule_id-austria/src/main/java/at/asitplus/eidas/specific/modules/auth/idaustria/tasks/ReceiveFromIdAustriaSystemTask.java')
-rw-r--r-- | modules/authmodule_id-austria/src/main/java/at/asitplus/eidas/specific/modules/auth/idaustria/tasks/ReceiveFromIdAustriaSystemTask.java | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/modules/authmodule_id-austria/src/main/java/at/asitplus/eidas/specific/modules/auth/idaustria/tasks/ReceiveFromIdAustriaSystemTask.java b/modules/authmodule_id-austria/src/main/java/at/asitplus/eidas/specific/modules/auth/idaustria/tasks/ReceiveFromIdAustriaSystemTask.java new file mode 100644 index 00000000..e59b0671 --- /dev/null +++ b/modules/authmodule_id-austria/src/main/java/at/asitplus/eidas/specific/modules/auth/idaustria/tasks/ReceiveFromIdAustriaSystemTask.java @@ -0,0 +1,370 @@ +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<PvpSProfileResponse, Boolean> 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<String> 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<String> 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, null, e.getMessage(), e); + + } + } + + + /** + * Check if mandate information is available. + * + * @param extractor Assertion from ID Austria system. + * @return <code>true</code> if mandate was used, otherwise <code>false</code> + */ + 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<PvpSProfileResponse, Boolean> preProcessAuthResponse(PvpSProfileResponse msg) + throws IOException, MarshallingException, TransformerException, + CredentialsNotAvailableException, AuthnResponseValidationException, SamlAssertionValidationExeption { + log.debug("Start PVP-2x assertion processing... "); + final Response samlResp = (Response) msg.getResponse(); + + // 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, + // EidasAuthEventConstants.AUTHPROCESS_EIDAS_AT_CONNECTOR_RECEIVED, + // 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, + // EidasAuthEventConstants.AUTHPROCESS_EIDAS_AT_CONNECTOR_RECEIVED_ERROR); + 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 <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; + + } + +} |