/******************************************************************************* * Copyright 2019 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.core.impl.idp.auth.services; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import javax.naming.ConfigurationException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import at.gv.egiz.components.eventlog.api.EventConstants; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.IRequestStorage; import at.gv.egiz.eaaf.core.api.IStatusMessenger; import at.gv.egiz.eaaf.core.api.data.EAAFConstants; import at.gv.egiz.eaaf.core.api.gui.IGUIBuilderConfiguration; import at.gv.egiz.eaaf.core.api.gui.IGUIBuilderConfigurationFactory; import at.gv.egiz.eaaf.core.api.gui.ISpringMVCGUIFormBuilder; import at.gv.egiz.eaaf.core.api.gui.ModifyableGuiBuilderConfiguration; import at.gv.egiz.eaaf.core.api.idp.IAction; import at.gv.egiz.eaaf.core.api.idp.IAuthData; import at.gv.egiz.eaaf.core.api.idp.IAuthenticationDataBuilder; import at.gv.egiz.eaaf.core.api.idp.IModulInfo; import at.gv.egiz.eaaf.core.api.idp.ISPConfiguration; import at.gv.egiz.eaaf.core.api.idp.auth.IAuthenticationManager; import at.gv.egiz.eaaf.core.api.idp.auth.ISSOManager; import at.gv.egiz.eaaf.core.api.idp.auth.services.IProtocolAuthenticationService; import at.gv.egiz.eaaf.core.api.idp.slo.SLOInformationInterface; import at.gv.egiz.eaaf.core.api.logging.IRevisionLogger; import at.gv.egiz.eaaf.core.api.logging.IStatisticLogger; import at.gv.egiz.eaaf.core.api.storage.ITransactionStorage; import at.gv.egiz.eaaf.core.api.utils.IPendingRequestIdGenerationStrategy; import at.gv.egiz.eaaf.core.exceptions.AuthnRequestValidatorException; import at.gv.egiz.eaaf.core.exceptions.EAAFAuthenticationException; import at.gv.egiz.eaaf.core.exceptions.EAAFException; import at.gv.egiz.eaaf.core.exceptions.EAAFSSOException; import at.gv.egiz.eaaf.core.exceptions.GUIBuildException; import at.gv.egiz.eaaf.core.exceptions.InvalidProtocolRequestException; import at.gv.egiz.eaaf.core.exceptions.ProcessExecutionException; import at.gv.egiz.eaaf.core.exceptions.ProtocolNotActiveException; import at.gv.egiz.eaaf.core.impl.gui.AbstractGUIFormBuilderConfiguration; import at.gv.egiz.eaaf.core.impl.idp.controller.protocols.RequestImpl; import at.gv.egiz.eaaf.core.impl.utils.HTTPUtils; @Service public class ProtocolAuthenticationService implements IProtocolAuthenticationService { private static final Logger log = LoggerFactory.getLogger(ProtocolAuthenticationService.class); @Autowired(required=true) private ApplicationContext applicationContext; @Autowired(required=true) private ITransactionStorage transactionStorage; @Autowired(required=true) private IAuthenticationManager authmanager; @Autowired(required=true) private IAuthenticationDataBuilder authDataBuilder; @Autowired(required=true) private ISpringMVCGUIFormBuilder guiBuilder; @Autowired(required=true) private IGUIBuilderConfigurationFactory guiConfigFactory; @Autowired(required=true) private IStatusMessenger statusMessager; @Autowired(required=true) private IRequestStorage requestStorage; @Autowired(required=true) IPendingRequestIdGenerationStrategy pendingReqIdGenerationStrategy; @Autowired(required=false) private ISSOManager ssoManager; @Autowired private IStatisticLogger statisticLogger; @Autowired private IRevisionLogger revisionsLogger; /* (non-Javadoc) * @see at.gv.egiz.eaaf.core.impl.idp.auth.services.IProtocolAuthenticationService#performAuthentication(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, at.gv.egiz.eaaf.core.api.IRequest) */ @Override public void performAuthentication(final HttpServletRequest req, final HttpServletResponse resp, final IRequest pendingReq) throws IOException, EAAFException { try { if (pendingReq.isNeedAuthentication()) { //request needs authentication --> start authentication process ... //set pendingRequestId to support asynchrony message-processing ((RequestImpl)pendingReq).setPendingRequestId(pendingReqIdGenerationStrategy.generateExternalPendingRequestId()); //load Parameters from OnlineApplicationConfiguration final ISPConfiguration oaParam = pendingReq.getServiceProviderConfiguration(); if (oaParam == null) throw new EAAFAuthenticationException( IStatusMessenger.CODES_INTERNAL_ERROR_AUTH_NOSPCONFIG, new Object[] { pendingReq.getSPEntityId() }); if (authmanager.doAuthentication(req, resp, pendingReq)) { //pending request is already authenticated --> protocol-specific postProcessing can start directly finalizeAuthentication(req, resp, pendingReq); //transaction is finished, log transaction finished event revisionsLogger.logEvent(EventConstants.TRANSACTION_DESTROYED, pendingReq.getUniqueTransactionIdentifier()); } } else { executeProtocolSpecificAction(req, resp, pendingReq, null); } } catch (final Exception e) { buildProtocolSpecificErrorResponse(e, req, resp, pendingReq); authmanager.performOnlyIDPLogOut(req, resp, pendingReq); } } /* (non-Javadoc) * @see at.gv.egiz.eaaf.core.impl.idp.auth.services.IProtocolAuthenticationService#finalizeAuthentication(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, at.gv.egiz.eaaf.core.api.IRequest) */ @Override public void finalizeAuthentication(final HttpServletRequest req, final HttpServletResponse resp, final IRequest pendingReq) throws EAAFException, IOException{ log.debug("Finalize PendingRequest with ID " + pendingReq.getPendingRequestId()); try { //check if pending-request has 'abortedByUser' flag set if (pendingReq.isAbortedByUser()) { //send authentication aborted error to Service Provider buildProtocolSpecificErrorResponse( new EAAFAuthenticationException( IStatusMessenger.CODES_INTERNAL_ERROR_AUTH_USERSTOP, new Object[] {}), req, resp, pendingReq); //do not remove the full active SSO-Session // in case of only one Service-Provider authentication request is aborted if ( !pendingReq.needSingleSignOnFunctionality()) { transactionStorage.remove(pendingReq.getPendingRequestId()); } //check if pending-request are authenticated } else if (pendingReq.isAuthenticated()) { internalFinalizeAuthenticationProcess(req, resp, pendingReq); } else { //suspect state: pending-request is not aborted but also are not authenticated log.error("PendingRequest is NOT authenticated --> Abort authentication process!"); handleErrorNoRedirect( new EAAFException( "auth.20", null), req, resp, true); } } catch (final Exception e) { log.error("Finalize authentication protocol FAILED." , e); buildProtocolSpecificErrorResponse(e, req, resp, pendingReq); if (pendingReq != null) transactionStorage.remove(pendingReq.getPendingRequestId()); } //remove pending-request if (pendingReq != null) { requestStorage.removePendingRequest(pendingReq.getPendingRequestId()); revisionsLogger.logEvent(EventConstants.TRANSACTION_DESTROYED, pendingReq.getUniqueTransactionIdentifier()); } } @Override public void buildProtocolSpecificErrorResponse(final Throwable throwable, final HttpServletRequest req, final HttpServletResponse resp, final IRequest protocolRequest) throws EAAFException, IOException { try { final Class clazz = Class.forName(protocolRequest.requestedModule()); if (clazz == null || !IModulInfo.class.isAssignableFrom(clazz)) { log.error("Requested protocol module Class is NULL or does not implement the IModulInfo interface."); throw new Exception("Requested protocol module Class is NULL or does not implement the IModulInfo interface."); } final IModulInfo handlingModule = (IModulInfo) applicationContext.getBean(clazz); if (handlingModule.generateErrorMessage( throwable, req, resp, protocolRequest)) { //log Error to technical log logExceptionToTechnicalLog(throwable); //log Error Message statisticLogger.logErrorOperation(throwable, protocolRequest); //write revision log entries revisionsLogger.logEvent(protocolRequest, EventConstants.TRANSACTION_ERROR, protocolRequest.getUniqueTransactionIdentifier()); return; } else { handleErrorNoRedirect(throwable, req, resp, true); } } catch (final Throwable e) { handleErrorNoRedirect(throwable, req, resp, true); } } @Override public void handleErrorNoRedirect(final Throwable throwable, final HttpServletRequest req, final HttpServletResponse resp, final boolean writeExceptionToStatisticLog) throws IOException, EAAFException { //log Exception into statistic database if (writeExceptionToStatisticLog) statisticLogger.logErrorOperation(throwable); //write errror to console logExceptionToTechnicalLog(throwable); //return error to Web browser if (throwable instanceof EAAFException || throwable instanceof ProcessExecutionException) internalMOAIDExceptionHandler(req, resp, (Exception)throwable, false); else { //write generic message for general exceptions final String msg = statusMessager.getMessage(IStatusMessenger.CODES_INTERNAL_ERROR_GENERIC, null); writeHTMLErrorResponse(req, resp, msg, "9199", null, (Exception) throwable); } } /** * Finalize the requested protocol operation * * @param httpReq HttpServletRequest * @param httpResp HttpServletResponse * @param protocolRequest Authentication request which is actually in process * @param moaSession MOASession object, which is used to generate the protocol specific authentication information * @throws Exception */ protected void internalFinalizeAuthenticationProcess(final HttpServletRequest req, final HttpServletResponse resp, final IRequest pendingReq) throws Exception { String newSSOSessionId = null; //if Single Sign-On functionality is enabled for this request if (pendingReq.needSingleSignOnFunctionality()) { if (ssoManager != null) { newSSOSessionId = ssoManager.createNewSSOSessionCookie(req, resp, pendingReq); if (StringUtils.isEmpty(pendingReq.getInternalSSOSessionIdentifier())) ssoManager.createNewSSOSession(pendingReq, newSSOSessionId); } else log.warn("SSO is requested but there is not SSO Session-Manager available"); } //build authenticationdata from session information and OA configuration final IAuthData authData = authDataBuilder.buildAuthenticationData(pendingReq); //execute the protocol-specific action final SLOInformationInterface sloInformation = executeProtocolSpecificAction(req, resp, pendingReq, authData); //Store OA specific SSO session information if an SSO cookie is set if (StringUtils.isNotEmpty(newSSOSessionId)) { try { ssoManager.updateSSOSession(pendingReq, newSSOSessionId, sloInformation); } catch (final EAAFSSOException e) { log.warn("SSO Session information can not be stored -> SSO is not enabled!"); authmanager.performOnlyIDPLogOut(req, resp, pendingReq); } } else { //remove MOASession from database authmanager.performOnlyIDPLogOut(req, resp, pendingReq); } //Advanced statistic logging statisticLogger.logSuccessOperation(pendingReq, authData, StringUtils.isNotEmpty(newSSOSessionId)); } /** * Executes the requested protocol action * * @param httpReq HttpServletRequest * @param httpResp HttpServletResponse * @param protocolRequest Authentication request which is actually in process * @param authData Service-provider specific authentication data * * @return Return Single LogOut information or null if protocol supports no SSO * * @throws Exception */ private SLOInformationInterface executeProtocolSpecificAction(final HttpServletRequest httpReq, final HttpServletResponse httpResp, final IRequest pendingReq, final IAuthData authData) throws Exception { try { // request needs no authentication --> start request processing final Class clazz = Class.forName(pendingReq.requestedAction()); if (clazz == null || !IAction.class.isAssignableFrom(clazz)) { log.error("Requested protocol-action processing Class is NULL or does not implement the IAction interface."); throw new Exception("Requested protocol-action processing Class is NULL or does not implement the IAction interface."); } final IAction protocolAction = (IAction) applicationContext.getBean(clazz); return protocolAction.processRequest(pendingReq, httpReq, httpResp, authData); } catch (final ClassNotFoundException e) { log.error("Requested Auth. protocol processing Class is NULL or does not implement the IAction interface."); throw new Exception("Requested Auth. protocol processing Class is NULL or does not implement the IAction interface."); } } /** * Write a Exception to the MOA-ID-Auth internal technical log * * @param loggedException Exception to log */ protected void logExceptionToTechnicalLog(final Throwable loggedException) { if (!( loggedException instanceof EAAFException || loggedException instanceof ProcessExecutionException )) { log.error("Receive an internal error: Message=" + loggedException.getMessage(), loggedException); } else { if (log.isDebugEnabled() || log.isTraceEnabled()) { log.warn(loggedException.getMessage(), loggedException); } else { log.warn(loggedException.getMessage()); } } } private void writeBadRequestErrorResponse(final HttpServletRequest req, final HttpServletResponse resp, final EAAFException e) throws IOException { final String code = statusMessager.mapInternalErrorToExternalError(((InvalidProtocolRequestException)e).getErrorId()); final String descr = StringEscapeUtils.escapeHtml4(StringEscapeUtils.escapeEcmaScript(e.getMessage())); resp.setContentType(EAAFConstants.CONTENTTYPE_HTML_UTF8); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Protocol validation FAILED!" + "(Errorcode=" + code + " | Description=" + descr + ")"); } private void writeHTMLErrorResponse(@NonNull final HttpServletRequest httpReq, @NonNull final HttpServletResponse httpResp, @NonNull final String msg, @NonNull final String errorCode, @Nullable final Object[] params, @NonNull final Exception error) throws IOException, EAAFException { try { final IGUIBuilderConfiguration config = guiConfigFactory.getDefaultErrorGUI(HTTPUtils.extractAuthURLFromRequest(httpReq)); String[] errorCodeParams = null; if (params == null) errorCodeParams = new String[] {}; else { errorCodeParams = new String[params.length]; for (int i=0; i