/******************************************************************************* * 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!"); } } }