/* * 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 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.credential.EaafX509Credential; import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IPvp2MetadataProvider; import at.gv.egiz.eaaf.modules.pvp2.api.utils.IPvp2CredentialProvider; 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.exception.SamlSigningException; 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.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 org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.NameIDType; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.Status; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.core.StatusMessage; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; import org.opensaml.xmlsec.signature.SignableXMLObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; public abstract class AbstractPvp2XProtocol extends AbstractController implements IModulInfo { private static final Logger log = LoggerFactory.getLogger(AbstractPvp2XProtocol.class); private static final String HTTP_PARAM_SAMLREQ = "SAMLRequest"; private static final String ERROR_INVALID_REQUEST = "Receive INVALID protocol request: {}"; @Autowired(required = true) protected IPvp2BasicConfiguration pvpBasicConfiguration; @Autowired(required = true) protected IPvp2MetadataProvider metadataProvider; @Autowired(required = true) protected SamlVerificationEngine samlVerificationEngine; @Autowired(required = false) protected List authRequestPostProcessors; private IPvp2CredentialProvider pvpIdpCredentials; /** * Sets a specific credential provider for PVP S-Profile IDP component. * * @param pvpIdpCredentials credential provider */ public void setPvpIdpCredentials(final IPvp2CredentialProvider pvpIdpCredentials) { this.pvpIdpCredentials = pvpIdpCredentials; } @Override public boolean generateErrorMessage(final Throwable e, final HttpServletRequest request, final HttpServletResponse response, final IRequest protocolRequest) throws Throwable { if (protocolRequest == null) { throw e; } if (!(protocolRequest instanceof PvpSProfilePendingRequest)) { throw e; } final PvpSProfilePendingRequest pvpRequest = (PvpSProfilePendingRequest) protocolRequest; final Response samlResponse = Saml2Utils.createSamlObject(Response.class); final Status status = Saml2Utils.createSamlObject(Status.class); final StatusCode statusCode = Saml2Utils.createSamlObject(StatusCode.class); final StatusMessage statusMessage = Saml2Utils.createSamlObject(StatusMessage.class); String moaError = null; if (e instanceof NoPassivAuthenticationException) { statusCode.setValue(StatusCode.NO_PASSIVE); statusMessage.setMessage(StringEscapeUtils.escapeXml(e.getLocalizedMessage())); } else if (e instanceof NameIdFormatNotSupportedException) { statusCode.setValue(StatusCode.INVALID_NAMEID_POLICY); 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) { final Pvp2Exception ex = (Pvp2Exception) e; statusCode.setValue(ex.getStatusCodeValue()); final String statusMessageValue = ex.getStatusMessageValue(); if (statusMessageValue != null) { statusMessage.setMessage(StringEscapeUtils.escapeXml(statusMessageValue)); } moaError = statusMessager.mapInternalErrorToExternalError(ex.getErrorId()); } else { statusCode.setValue(StatusCode.RESPONDER); statusMessage.setMessage(StringEscapeUtils.escapeXml(e.getLocalizedMessage())); moaError = statusMessager.getResponseErrorCode(e); } if (StringUtils.isNotEmpty(moaError)) { final 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); final String remoteSessionID = Saml2Utils.getSecureIdentifier(); samlResponse.setID(remoteSessionID); samlResponse.setIssueInstant(new DateTime()); final Issuer nissuer = Saml2Utils.createSamlObject(Issuer.class); nissuer.setValue(pvpBasicConfiguration.getIdpEntityId(pvpRequest.getAuthUrl())); nissuer.setFormat(NameIDType.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(); } final EaafX509Credential signCred = pvpIdpCredentials.getMessageSigningCredential(); encoder.encodeResponse(request, response, samlResponse, pvpRequest.getConsumerUrl(), relayState, signCred, protocolRequest); return true; } @Override public boolean validate(final HttpServletRequest request, final HttpServletResponse response, final IRequest pending) { return true; } protected void pvpMetadataRequest(final HttpServletRequest req, final HttpServletResponse resp) throws EaafException { // create pendingRequest object final PvpSProfilePendingRequest pendingReq = applicationContext.getBean(PvpSProfilePendingRequest.class); pendingReq.initialize(req, authConfig); pendingReq.setModule(getName()); revisionsLogger.logEvent(pendingReq.getUniqueSessionIdentifier(), pendingReq.getUniqueTransactionIdentifier(), EventConstants.TRANSACTION_IP, req.getRemoteAddr()); final MetadataAction metadataAction = applicationContext.getBean(MetadataAction.class); metadataAction.processRequest(pendingReq, req, resp, null); } protected void pvpIdpPostRequest(final HttpServletRequest req, final 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 final InboundMessage msg = (InboundMessage) new PostBinding().decode(req, resp, metadataProvider, SPSSODescriptor.DEFAULT_ELEMENT_NAME, new EaafUriCompare(pvpBasicConfiguration.getIdpSsoPostService(pendingReq.getAuthUrl()))); pendingReq.setRequest(msg); // preProcess Message preProcess(req, resp, pendingReq); } catch (final SamlSigningException e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.warn(ERROR_INVALID_REQUEST, samlRequest, null, e); // write revision log entries if (pendingReq != null) { revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); } throw new InvalidProtocolRequestException("pvp2.21", new Object[] {}); } catch (final Pvp2Exception e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.warn(ERROR_INVALID_REQUEST, samlRequest, null, 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 (final EaafException e) { // write revision log entries if (pendingReq != null) { revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); } throw e; } catch (final Throwable e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.warn(ERROR_INVALID_REQUEST, samlRequest, null, 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(final HttpServletRequest req, final 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 final InboundMessage msg = (InboundMessage) new RedirectBinding().decode(req, resp, metadataProvider, SPSSODescriptor.DEFAULT_ELEMENT_NAME, new EaafUriCompare( pvpBasicConfiguration.getIdpSsoRedirectService(pendingReq.getAuthUrl()))); pendingReq.setRequest(msg); // preProcess Message preProcess(req, resp, pendingReq); } catch (final SamlSigningException e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.warn(ERROR_INVALID_REQUEST, samlRequest, null, e); // write revision log entries if (pendingReq != null) { revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); } throw new InvalidProtocolRequestException("pvp2.21", new Object[] {}); } catch (final Pvp2Exception e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.warn(ERROR_INVALID_REQUEST, samlRequest, null, 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 (final EaafException e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.info(ERROR_INVALID_REQUEST, samlRequest, null, e); // write revision log entries if (pendingReq != null) { revisionsLogger.logEvent(pendingReq, EventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); } throw e; } catch (final Throwable e) { final String samlRequest = req.getParameter(HTTP_PARAM_SAMLREQ); log.warn(ERROR_INVALID_REQUEST, samlRequest, null, 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); } } /** * Authentication request pre-processor. * * @param request http request * @param response http response * @param pendingReq current pending request * @return true if preprocess can handle this request type, otherwise false * @throws Throwable In case of an error */ protected abstract boolean childPreProcess(HttpServletRequest request, HttpServletResponse response, PvpSProfilePendingRequest pendingReq) throws Throwable; protected void preProcess(final HttpServletRequest request, final HttpServletResponse response, final PvpSProfilePendingRequest pendingReq) throws Throwable { final 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, 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 http request * @param pendingReq current pending request * @throws Throwable in case of an error */ protected void preProcessAuthRequest(final HttpServletRequest request, final PvpSProfilePendingRequest pendingReq) throws Throwable { final PvpSProfileRequest moaRequest = (PvpSProfileRequest) pendingReq.getRequest(); final SignableXMLObject samlReq = moaRequest.getSamlRequest(); if (!(samlReq instanceof AuthnRequest)) { throw new InvalidPvpRequestException("Unsupported request", new Object[] {}); } final EntityDescriptor metadata = moaRequest.getEntityMetadata(metadataProvider); if (metadata == null) { throw new NoMetadataInformationException(); } final SPSSODescriptor spSsoDescriptor = metadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS); final 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 final List metadataAssertionServiceList = spSsoDescriptor.getAssertionConsumerServices(); for (final 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 final Integer aIdx = authnRequest.getAssertionConsumerServiceIndex(); int assertionidx = 0; if (aIdx != null) { assertionidx = aIdx; } else { assertionidx = Saml2Utils.getDefaultAssertionConsumerServiceIndex(spSsoDescriptor); } consumerService = spSsoDescriptor.getAssertionConsumerServices().get(assertionidx); if (consumerService == null) { throw new InvalidAssertionConsumerServiceException(aIdx); } } // validate AuthnRequest final AuthnRequest authReq = (AuthnRequest) samlReq; final 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!"); } } }