From bd6492ef3f4576d62b8f7cfe20dbaf425b22e2bb Mon Sep 17 00:00:00 2001 From: Thomas Lenz Date: Thu, 12 Dec 2019 14:52:41 +0100 Subject: back-port of PVP IDP-module request post-processor --- .../pvp2/idp/impl/AbstractPVP2XProtocol.java | 549 +++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 eaaf_modules/eaaf_module_pvp2_idp/src/main/java/at/gv/egiz/eaaf/modules/pvp2/idp/impl/AbstractPVP2XProtocol.java (limited to 'eaaf_modules/eaaf_module_pvp2_idp') diff --git a/eaaf_modules/eaaf_module_pvp2_idp/src/main/java/at/gv/egiz/eaaf/modules/pvp2/idp/impl/AbstractPVP2XProtocol.java b/eaaf_modules/eaaf_module_pvp2_idp/src/main/java/at/gv/egiz/eaaf/modules/pvp2/idp/impl/AbstractPVP2XProtocol.java new file mode 100644 index 00000000..597d3c22 --- /dev/null +++ b/eaaf_modules/eaaf_module_pvp2_idp/src/main/java/at/gv/egiz/eaaf/modules/pvp2/idp/impl/AbstractPVP2XProtocol.java @@ -0,0 +1,549 @@ +/******************************************************************************* + * Copyright 2017 Graz University of Technology + * EAAF-Core Components has been developed in a cooperation between EGIZ, + * A-SIT Plus, 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 "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/news/understanding-eupl-v12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + * 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.gv.egiz.eaaf.modules.pvp2.idp.impl; + +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.Status; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.StatusMessage; +import org.opensaml.saml2.metadata.AssertionConsumerService; +import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.SPSSODescriptor; +import org.opensaml.ws.security.SecurityPolicyException; +import org.opensaml.xml.security.x509.X509Credential; +import org.opensaml.xml.signature.SignableXMLObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import at.gv.egiz.components.eventlog.api.EventConstants; +import at.gv.egiz.eaaf.core.api.IRequest; +import at.gv.egiz.eaaf.core.api.data.EAAFConstants; +import at.gv.egiz.eaaf.core.api.idp.IModulInfo; +import at.gv.egiz.eaaf.core.api.logging.IRevisionLogger; +import at.gv.egiz.eaaf.core.exceptions.AuthnRequestValidatorException; +import at.gv.egiz.eaaf.core.exceptions.EAAFException; +import at.gv.egiz.eaaf.core.exceptions.InvalidProtocolRequestException; +import at.gv.egiz.eaaf.core.exceptions.NoPassivAuthenticationException; +import at.gv.egiz.eaaf.core.exceptions.SLOException; +import at.gv.egiz.eaaf.core.impl.idp.controller.AbstractController; +import at.gv.egiz.eaaf.modules.pvp2.PVPEventConstants; +import at.gv.egiz.eaaf.modules.pvp2.api.IPVP2BasicConfiguration; +import at.gv.egiz.eaaf.modules.pvp2.api.binding.IEncoder; +import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IPVPMetadataProvider; +import at.gv.egiz.eaaf.modules.pvp2.api.validation.IAuthnRequestPostProcessor; +import at.gv.egiz.eaaf.modules.pvp2.exception.InvalidPVPRequestException; +import at.gv.egiz.eaaf.modules.pvp2.exception.NameIDFormatNotSupportedException; +import at.gv.egiz.eaaf.modules.pvp2.exception.NoMetadataInformationException; +import at.gv.egiz.eaaf.modules.pvp2.exception.PVP2Exception; +import at.gv.egiz.eaaf.modules.pvp2.idp.exception.InvalidAssertionConsumerServiceException; +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.binding.SoapBinding; +import at.gv.egiz.eaaf.modules.pvp2.impl.message.InboundMessage; +import at.gv.egiz.eaaf.modules.pvp2.impl.message.PVPSProfileRequest; +import at.gv.egiz.eaaf.modules.pvp2.impl.utils.AbstractCredentialProvider; +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; + +public abstract class AbstractPVP2XProtocol extends AbstractController implements IModulInfo { + private static final Logger log = LoggerFactory.getLogger(AbstractPVP2XProtocol.class); + + @Autowired(required=true) protected IPVP2BasicConfiguration pvpBasicConfiguration; + @Autowired(required=true) protected IPVPMetadataProvider metadataProvider; + @Autowired(required=true) protected SAMLVerificationEngine samlVerificationEngine; + @Autowired(required=false) protected List authRequestPostProcessors; + + private AbstractCredentialProvider pvpIDPCredentials; + + + + /** + * Sets a specific credential provider for PVP S-Profile IDP component. + * @param pvpIDPCredentials credential provider + */ + public void setPvpIDPCredentials(AbstractCredentialProvider pvpIDPCredentials) { + this.pvpIDPCredentials = pvpIDPCredentials; + + } + + public boolean generateErrorMessage(Throwable e, + HttpServletRequest request, HttpServletResponse response, + IRequest protocolRequest) throws Throwable { + + if(protocolRequest == null) { + throw e; + } + + if(!(protocolRequest instanceof PVPSProfilePendingRequest) ) { + throw e; + } + PVPSProfilePendingRequest pvpRequest = (PVPSProfilePendingRequest)protocolRequest; + + Response samlResponse = + SAML2Utils.createSAMLObject(Response.class); + Status status = SAML2Utils.createSAMLObject(Status.class); + StatusCode statusCode = SAML2Utils.createSAMLObject(StatusCode.class); + StatusMessage statusMessage = SAML2Utils.createSAMLObject(StatusMessage.class); + + String moaError = null; + + if(e instanceof NoPassivAuthenticationException) { + statusCode.setValue(StatusCode.NO_PASSIVE_URI); + statusMessage.setMessage(StringEscapeUtils.escapeXml(e.getLocalizedMessage())); + + } else if (e instanceof NameIDFormatNotSupportedException) { + statusCode.setValue(StatusCode.INVALID_NAMEID_POLICY_URI); + statusMessage.setMessage(StringEscapeUtils.escapeXml(e.getLocalizedMessage())); + + } else if (e instanceof SLOException) { + //SLOExecpetions only occurs if session information is lost + return false; + + } else if(e instanceof PVP2Exception) { + PVP2Exception ex = (PVP2Exception) e; + statusCode.setValue(ex.getStatusCodeValue()); + String statusMessageValue = ex.getStatusMessageValue(); + if(statusMessageValue != null) { + statusMessage.setMessage(StringEscapeUtils.escapeXml(statusMessageValue)); + } + moaError = statusMessager.mapInternalErrorToExternalError(ex.getErrorId()); + + } else { + statusCode.setValue(StatusCode.RESPONDER_URI); + statusMessage.setMessage(StringEscapeUtils.escapeXml(e.getLocalizedMessage())); + moaError = statusMessager.getResponseErrorCode(e); + } + + + if (StringUtils.isNotEmpty(moaError)) { + StatusCode moaStatusCode = SAML2Utils.createSAMLObject(StatusCode.class); + moaStatusCode.setValue(moaError); + statusCode.setStatusCode(moaStatusCode); + } + + status.setStatusCode(statusCode); + if(statusMessage.getMessage() != null) { + status.setStatusMessage(statusMessage); + } + samlResponse.setStatus(status); + String remoteSessionID = SAML2Utils.getSecureIdentifier(); + samlResponse.setID(remoteSessionID); + + samlResponse.setIssueInstant(new DateTime()); + Issuer nissuer = SAML2Utils.createSAMLObject(Issuer.class); + nissuer.setValue(pvpBasicConfiguration.getIDPEntityId(pvpRequest.getAuthURL())); + nissuer.setFormat(NameID.ENTITY); + samlResponse.setIssuer(nissuer); + + IEncoder encoder = null; + + if(pvpRequest.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) { + encoder = applicationContext.getBean("PVPRedirectBinding", RedirectBinding.class); + + } else if(pvpRequest.getBinding().equals(SAMLConstants.SAML2_POST_BINDING_URI)) { + encoder = applicationContext.getBean("PVPPOSTBinding", PostBinding.class); + + } else if (pvpRequest.getBinding().equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) { + encoder = applicationContext.getBean("PVPSOAPBinding", SoapBinding.class); + } + + if(encoder == null) { + // default to redirect binding + encoder = new RedirectBinding(); + } + + String relayState = null; + if (pvpRequest.getRequest() != null) + relayState = pvpRequest.getRequest().getRelayState(); + + X509Credential signCred = pvpIDPCredentials.getIDPAssertionSigningCredential(); + + encoder.encodeRespone(request, response, samlResponse, pvpRequest.getConsumerURL(), + relayState, signCred, protocolRequest); + return true; + } + + public boolean validate(HttpServletRequest request, + HttpServletResponse response, IRequest pending) { + + return true; + } + + protected void pvpMetadataRequest(HttpServletRequest req, HttpServletResponse resp) throws EAAFException { + //create pendingRequest object + PVPSProfilePendingRequest pendingReq = applicationContext.getBean(PVPSProfilePendingRequest.class); + pendingReq.initialize(req, authConfig); + pendingReq.setModule(getName()); + + revisionsLogger.logEvent( + pendingReq.getUniqueSessionIdentifier(), + pendingReq.getUniqueTransactionIdentifier(), + EventConstants.TRANSACTION_IP, + req.getRemoteAddr()); + + MetadataAction metadataAction = applicationContext.getBean(MetadataAction.class); + metadataAction.processRequest(pendingReq, + req, resp, null); + + } + + protected void PVPIDPPostRequest(HttpServletRequest req, HttpServletResponse resp) throws EAAFException { + PVPSProfilePendingRequest pendingReq = null; + + try { + //create pendingRequest object + pendingReq = applicationContext.getBean(PVPSProfilePendingRequest.class); + pendingReq.initialize(req, authConfig); + pendingReq.setModule(getName()); + + revisionsLogger.logEvent(EventConstants.SESSION_CREATED, pendingReq.getUniqueSessionIdentifier()); + revisionsLogger.logEvent(EventConstants.TRANSACTION_CREATED, pendingReq.getUniqueTransactionIdentifier()); + revisionsLogger.logEvent( + pendingReq.getUniqueSessionIdentifier(), + pendingReq.getUniqueTransactionIdentifier(), + EventConstants.TRANSACTION_IP, + req.getRemoteAddr()); + + //get POST-Binding decoder implementation + InboundMessage msg = (InboundMessage) new PostBinding().decode( + req, resp, metadataProvider, false, + new EAAFURICompare(pvpBasicConfiguration.getIDPSSOPostService(pendingReq.getAuthURL()))); + pendingReq.setRequest(msg); + + //preProcess Message + preProcess(req, resp, pendingReq); + + } catch (SecurityPolicyException e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.warn("Receive INVALID protocol request: " + samlRequest, e); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw new InvalidProtocolRequestException("pvp2.21", new Object[] {}); + + } catch (SecurityException e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.warn("Receive INVALID protocol request: " + samlRequest, e); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw new InvalidProtocolRequestException("pvp2.22", new Object[] {e.getMessage()}); + + } catch (EAAFException e) { + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw e; + + } catch (Throwable e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.warn("Receive INVALID protocol request: " + samlRequest, e); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw new EAAFException("pvp2.24", new Object[] {e.getMessage()}, e); + } + } + + protected void PVPIDPRedirecttRequest(HttpServletRequest req, HttpServletResponse resp) throws EAAFException { + PVPSProfilePendingRequest pendingReq = null; + try { + //create pendingRequest object + pendingReq = applicationContext.getBean(PVPSProfilePendingRequest.class); + pendingReq.initialize(req, authConfig); + pendingReq.setModule(getName()); + + revisionsLogger.logEvent(EventConstants.SESSION_CREATED, pendingReq.getUniqueSessionIdentifier()); + revisionsLogger.logEvent(EventConstants.TRANSACTION_CREATED, pendingReq.getUniqueTransactionIdentifier()); + revisionsLogger.logEvent( + pendingReq.getUniqueSessionIdentifier(), + pendingReq.getUniqueTransactionIdentifier(), + EventConstants.TRANSACTION_IP, + req.getRemoteAddr()); + + //get POST-Binding decoder implementation + InboundMessage msg = (InboundMessage) new RedirectBinding().decode( + req, resp, metadataProvider, false, + new EAAFURICompare(pvpBasicConfiguration.getIDPSSORedirectService(pendingReq.getAuthURL()))); + pendingReq.setRequest(msg); + + //preProcess Message + preProcess(req, resp, pendingReq); + + } catch (SecurityPolicyException e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.warn("Receive INVALID protocol request: " + samlRequest, e); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw new InvalidProtocolRequestException("pvp2.21", new Object[] {}); + + } catch (SecurityException e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.warn("Receive INVALID protocol request: " + samlRequest, e); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw new InvalidProtocolRequestException("pvp2.22", new Object[] {e.getMessage()}); + + } catch (EAAFException e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.info("Receive INVALID protocol request: " + samlRequest); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw e; + + } catch (Throwable e) { + String samlRequest = req.getParameter("SAMLRequest"); + log.warn("Receive INVALID protocol request: " + samlRequest, e); + + //write revision log entries + if (pendingReq != null) + revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); + + throw new EAAFException("pvp2.24", new Object[] {e.getMessage()}, e); + } + } + + + + /** + * + * + * @param request + * @param response + * @param msg + * @return true if preprocess can handle this request type, otherwise false + * @throws Throwable + */ + abstract protected boolean childPreProcess(HttpServletRequest request, + HttpServletResponse response, PVPSProfilePendingRequest pendingReq) throws Throwable; + + protected void preProcess(HttpServletRequest request, + HttpServletResponse response, PVPSProfilePendingRequest pendingReq) throws Throwable { + + InboundMessage msg = pendingReq.getRequest(); + + if (StringUtils.isEmpty(msg.getEntityID())) { + throw new InvalidProtocolRequestException("pvp2.20", new Object[] {}); + + } + + if(!msg.isVerified()) { + samlVerificationEngine.verify(msg, + TrustEngineFactory.getSignatureKnownKeysTrustEngine(metadataProvider)); + msg.setVerified(true); + + } + + revisionsLogger.logEvent(pendingReq, IRevisionLogger.AUTHPROTOCOL_TYPE, getAuthProtocolIdentifier()); + + if (msg instanceof PVPSProfileRequest && + ((PVPSProfileRequest)msg).getSamlRequest() instanceof AuthnRequest) + preProcessAuthRequest(request, response, pendingReq); + + else if (childPreProcess(request, response, pendingReq)) + log.debug("Find protocol handler in child implementation"); + + else { + log.error("Receive unsupported PVP21 message of type: " + ((PVPSProfileRequest)msg).getSamlRequest().getClass().getName()); + throw new InvalidPVPRequestException("pvp2.09", + new Object[] {((PVPSProfileRequest)msg).getSamlRequest().getClass().getName()}); + } + + + + //switch to session authentication + protAuthService.performAuthentication(request, response, pendingReq); + } + + + /** + * PreProcess Authn request + * @param request + * @param response + * @param pendingReq + * @throws Throwable + */ + private void preProcessAuthRequest(HttpServletRequest request, + HttpServletResponse response, PVPSProfilePendingRequest pendingReq) throws Throwable { + + PVPSProfileRequest moaRequest = ((PVPSProfileRequest)pendingReq.getRequest()); + SignableXMLObject samlReq = moaRequest.getSamlRequest(); + + if(!(samlReq instanceof AuthnRequest)) { + throw new InvalidPVPRequestException("Unsupported request", new Object[] {}); + } + + EntityDescriptor metadata = moaRequest.getEntityMetadata(metadataProvider); + if(metadata == null) { + throw new NoMetadataInformationException(); + } + SPSSODescriptor spSSODescriptor = metadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS); + + AuthnRequest authnRequest = (AuthnRequest)samlReq; + + if (authnRequest.getIssueInstant() == null) { + log.warn("Unsupported request: No IssueInstant Attribute found."); + throw new AuthnRequestValidatorException("pvp2.22", + new Object[] {"Unsupported request: No IssueInstant Attribute found"}, + pendingReq); + + } + + if (authnRequest.getIssueInstant().minusMinutes(EAAFConstants.ALLOWED_TIME_JITTER).isAfterNow()) { + log.warn("Unsupported request: No IssueInstant DateTime is not valid anymore."); + throw new AuthnRequestValidatorException("pvp2.22", + new Object[] {"Unsupported request: No IssueInstant DateTime is not valid anymore."}, + pendingReq); + + } + + //parse AssertionConsumerService + AssertionConsumerService consumerService = null; + if (StringUtils.isNotEmpty(authnRequest.getAssertionConsumerServiceURL()) && + StringUtils.isNotEmpty(authnRequest.getProtocolBinding())) { + //use AssertionConsumerServiceURL from request + + //check requested AssertionConsumingService URL against metadata + List metadataAssertionServiceList = spSSODescriptor.getAssertionConsumerServices(); + for (AssertionConsumerService service : metadataAssertionServiceList) { + if (authnRequest.getProtocolBinding().equals(service.getBinding()) + && authnRequest.getAssertionConsumerServiceURL().equals(service.getLocation())) { + consumerService = SAML2Utils.createSAMLObject(AssertionConsumerService.class); + consumerService.setBinding(authnRequest.getProtocolBinding()); + consumerService.setLocation(authnRequest.getAssertionConsumerServiceURL()); + log.debug("Requested AssertionConsumerServiceURL is valid."); + } + } + + if (consumerService == null) { + throw new InvalidAssertionConsumerServiceException(authnRequest.getAssertionConsumerServiceURL()); + + } + + + } else { + //use AssertionConsumerServiceIndex and select consumerService from metadata + Integer aIdx = authnRequest.getAssertionConsumerServiceIndex(); + int assertionidx = 0; + + if(aIdx != null) { + assertionidx = aIdx.intValue(); + + } else { + assertionidx = SAML2Utils.getDefaultAssertionConsumerServiceIndex(spSSODescriptor); + + } + consumerService = spSSODescriptor.getAssertionConsumerServices().get(assertionidx); + + if (consumerService == null) { + throw new InvalidAssertionConsumerServiceException(aIdx); + + } + } + + + //validate AuthnRequest + AuthnRequest authReq = (AuthnRequest) samlReq; + String oaURL = moaRequest.getEntityMetadata(metadataProvider).getEntityID(); + log.info("Dispatch PVP2 AuthnRequest: OAURL=" + oaURL + " Binding=" + consumerService.getBinding()); + + pendingReq.setSPEntityId(StringEscapeUtils.escapeHtml(oaURL)); + pendingReq.setOnlineApplicationConfiguration(authConfig.getServiceProviderConfiguration(pendingReq.getSPEntityId())); + pendingReq.setBinding(consumerService.getBinding()); + pendingReq.setRequest(moaRequest); + pendingReq.setConsumerURL(consumerService.getLocation()); + + //parse AuthRequest + pendingReq.setPassiv(authReq.isPassive()); + pendingReq.setForce(authReq.isForceAuthn()); + + //AuthnRequest needs authentication + pendingReq.setNeedAuthentication(true); + + //set protocol action, which should be executed after authentication + pendingReq.setAction(AuthenticationAction.class.getName()); + + // do post-processing if required + log.trace("Starting extended AuthnRequest validation and processing ... "); + if (authRequestPostProcessors != null) { + for (final IAuthnRequestPostProcessor processor : authRequestPostProcessors) { + log.trace("Post-process AuthnRequest with module: {}", processor.getClass().getSimpleName()); + processor.process(request, pendingReq, authReq, spSSODescriptor); + + } + } + + log.debug("Extended AuthnRequest validation and processing finished"); + + //write revisionslog entry + revisionsLogger.logEvent(pendingReq, PVPEventConstants.AUTHPROTOCOL_PVP_REQUEST_AUTHREQUEST, authReq.getID()); + + } + + @PostConstruct + private void verifyInitialization() { + if (pvpIDPCredentials == null) { + log.error("No SAML2 credentialProvider injected!"); + throw new RuntimeException("No SAML2 credentialProvider injected!"); + + } + } + +} -- cgit v1.2.3