/* * 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 java.util.Arrays; import java.util.List; 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.IGuiFormBuilder; 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.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.http.HttpUtils; import at.gv.egiz.eaaf.core.impl.idp.controller.protocols.RequestImpl; @Service public class ProtocolAuthenticationService implements IProtocolAuthenticationService { private static final Logger log = LoggerFactory.getLogger(ProtocolAuthenticationService.class); private static final List ERROR_LOGGER_ON_INFO_LEVEL = Arrays.asList(IStatusMessenger.CODES_INTERNAL_ERROR_AUTH_USERSTOP); @Autowired(required = true) private ApplicationContext applicationContext; @Autowired(required = true) private IAuthenticationManager authmanager; @Autowired(required = true) private IAuthenticationDataBuilder authDataBuilder; @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; private IGuiFormBuilder guiBuilder; /* * (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()) { requestStorage.removePendingRequest(pendingReq.getPendingRequestId()); } // check if pending-request are authenticated } else if (pendingReq.isAuthenticated() && !pendingReq.isNeedUserConsent()) { internalFinalizeAuthenticationProcess(req, resp, pendingReq); } else { // suspect state: pending-request is not aborted but also are not authenticated log.warn("PendingRequest flag for 'authenticated':{} and 'needConsent':{}", pendingReq.isAuthenticated(), pendingReq.isNeedUserConsent()); if (pendingReq.isNeedUserConsent()) { log.error( "PendingRequest NEEDS user-consent. Can NOT fininalize authentication --> Abort authentication process!"); } else { 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); } // remove pending-request 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); } } public void setGuiBuilder(final IGuiFormBuilder guiBuilder) { this.guiBuilder = guiBuilder; } /** * 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 In case of an error */ 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 in case of an error */ 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 (loggedException instanceof EaafAuthenticationException && ERROR_LOGGER_ON_INFO_LEVEL .contains(((EaafAuthenticationException) loggedException).getErrorId())) { if (log.isDebugEnabled() || log.isTraceEnabled()) { log.info(loggedException.getMessage(), loggedException); } else { log.info(loggedException.getMessage()); } } else { if (log.isDebugEnabled() || log.isTraceEnabled()) { log.warn(loggedException.getMessage(), loggedException); } else { log.warn(loggedException.getMessage()); } } } } 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 < params.length; i++) { if (params[i] != null) { errorCodeParams[i] = params[i].toString(); } else { errorCodeParams[i] = "null"; } } } // add errorcode and errormessage if (config instanceof ModifyableGuiBuilderConfiguration) { ((ModifyableGuiBuilderConfiguration) config).putCustomParameter( AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERROMSG, msg); ((ModifyableGuiBuilderConfiguration) config).putCustomParameter( AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERRORCODE, errorCode); ((ModifyableGuiBuilderConfiguration) config).putCustomParameterWithOutEscaption( AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERRORCODEPARAMS, ArrayUtils.toString(errorCodeParams)); // add stacktrace if debug is enabled if (log.isTraceEnabled()) { ((ModifyableGuiBuilderConfiguration) config).putCustomParameter( AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERRORSTACKTRACE, getStacktraceFromException(error)); } } else { log.info( "Can not ADD error message, because 'GUIBuilderConfiguration' is not modifieable "); } guiBuilder.build(httpReq, httpResp, config, "Error-Message"); } catch (final GuiBuildException e) { log.warn("Can not build error-message GUI.", e); throw new EaafException("9199", null, e); } } private String getStacktraceFromException(final Exception ex) { final StringWriter errors = new StringWriter(); ex.printStackTrace(new PrintWriter(errors)); return errors.toString(); } private void internalMoaidExceptionHandler(final HttpServletRequest req, final HttpServletResponse resp, final Exception e, final boolean writeExceptionToStatisicLog) throws IOException, EaafException { if (e instanceof ProtocolNotActiveException) { resp.getWriter().write(e.getMessage()); resp.setContentType(EaafConstants.CONTENTTYPE_HTML_UTF8); resp.sendError(HttpServletResponse.SC_FORBIDDEN, StringEscapeUtils.escapeHtml4(StringEscapeUtils.escapeEcmaScript(e.getMessage()))); } else if (e instanceof AuthnRequestValidatorException) { final AuthnRequestValidatorException ex = (AuthnRequestValidatorException) e; // log Error Message if (writeExceptionToStatisicLog) { statisticLogger.logErrorOperation(ex, ex.getErrorRequest()); } // write error message // writeBadRequestErrorResponse(req, resp, (EAAFException) e); writeHtmlErrorResponse(req, resp, e.getMessage(), statusMessager.getResponseErrorCode(e), null, e); } else if (e instanceof InvalidProtocolRequestException) { // send error response // writeBadRequestErrorResponse(req, resp, (EAAFException) e); writeHtmlErrorResponse(req, resp, e.getMessage(), statusMessager.getResponseErrorCode(e), null, e); } else if (e instanceof ConfigurationException) { // send HTML formated error message writeHtmlErrorResponse(req, resp, e.getMessage(), statusMessager.getResponseErrorCode(e), null, e); } else if (e instanceof EaafException) { // send HTML formated error message writeHtmlErrorResponse(req, resp, e.getMessage(), statusMessager.getResponseErrorCode(e), ((EaafException) e).getParams(), e); } else if (e instanceof ProcessExecutionException) { // send HTML formated error message writeHtmlErrorResponse(req, resp, e.getMessage(), statusMessager.getResponseErrorCode(e), null, e); } } }