/* * 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 javax.annotation.PostConstruct; 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.owasp.encoder.Encode; 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.data.ExceptionContainer; 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.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.data.Pair; 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.auth.services.IErrorService.ActionType; import at.gv.egiz.eaaf.core.impl.idp.auth.services.IErrorService.IHandleData; import at.gv.egiz.eaaf.core.impl.idp.auth.services.IErrorService.LogLevel; import at.gv.egiz.eaaf.core.impl.idp.controller.ProtocolFinalizationController; import at.gv.egiz.eaaf.core.impl.idp.controller.protocols.RequestImpl; import at.gv.egiz.eaaf.core.impl.utils.ServletUtils; @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 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 = true) private IErrorService errorTicketService; @Autowired(required = false) private ISsoManager ssoManager; @Autowired private IStatisticLogger statisticLogger; @Autowired private IRevisionLogger revisionsLogger; @Autowired(required = true) protected ITransactionStorage transactionStorage; 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); // 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.info("Finalize authentication protocol FAILED. Reason: {}", e.getMessage()); buildProtocolSpecificErrorResponse(e, req, resp, pendingReq); } finally { // 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 IErrorService.IHandleData errorData = errorTicketService.createHandleData(throwable, protocolRequest); // log Error to technical log logExceptionToTechnicalLog(errorData); // log Error Message statisticLogger.logErrorOperation(throwable, protocolRequest); // write revision log entries revisionsLogger.logEvent(protocolRequest, EventConstants.TRANSACTION_ERROR, protocolRequest.getUniqueTransactionIdentifier()); if (ActionType.TICKET.equals(errorData.getActionType()) || ActionType.ERRORPAGE.equals(errorData.getActionType())) { if (errorData.getErrorIdTokenForRedirect() != null) { // Put pending request final ExceptionContainer exceptionContainer = new ExceptionContainer(protocolRequest, throwable); log.debug("Put error into cache to support SP forwarding ... "); String internalErrorToken = pendingReqIdGenerationStrategy.getPendingRequestIdWithOutChecks( errorData.getErrorIdTokenForRedirect()); log.trace("errorIdToken: {}", internalErrorToken); transactionStorage.put(internalErrorToken, exceptionContainer, -1); } else { log.debug("No errorTokenId. Forwarding to SP will not be available"); } // render GUI displayException(req, resp, errorData); } else { final IModulInfo handlingModule = extractShibbolethHandling(protocolRequest, applicationContext); if (handlingModule.generateErrorMessage(throwable, req, resp, protocolRequest)) { log.debug("Error-response to SP successfully written"); } else { log.info("Error-response to SP FAILED. Writing error message into GUI ... "); displayException(req, resp, errorData); } } } catch (final Throwable e) { // if building error response results in error, we try with with // handleErrorNoRedirect log.info("ErrorHandling has an internal error. Show process-error in GUI ... ", e); handleErrorNoRedirect(throwable, req, resp, false); } } /** * Retrieves shibboleth module info. * * @param protocolRequest current request * @param applicationContext spring context * @return IModulInfo * @throws ClassNotFoundException If no shibboleth handling implementation found */ public static IModulInfo extractShibbolethHandling(IRequest protocolRequest, ApplicationContext applicationContext) throws ClassNotFoundException { 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 ClassCastException( "Requested protocol module Class is NULL or does not implement the IModulInfo interface."); } return (IModulInfo) applicationContext.getBean(clazz); } @Override public void handleErrorNoRedirect(final Throwable throwable, final HttpServletRequest req, final HttpServletResponse resp, final boolean writeExceptionToStatisticLog) throws EaafException, IOException { final IErrorService.IHandleData errorData = errorTicketService.createHandleData(throwable, null); // log Exception into statistic database if (writeExceptionToStatisticLog) { statisticLogger.logErrorOperation(throwable); } // write errror to console logExceptionToTechnicalLog(errorData); // render GUI displayException(req, resp, errorData); } private void logExceptionToTechnicalLog(IHandleData errorData) { // In case of a TaskExecutionException, which is only a container for // process-errors, // extract internal exception // Log exception if (!(errorData.getThrowable() instanceof EaafException) || LogLevel.ERROR.equals(errorData.getLogLevel())) { log.error(errorData.getPreFormatedErrorMessage(), errorData.getThrowable()); } else if (LogLevel.WARN.equals(errorData.getLogLevel())) { log.warn(errorData.getPreFormatedErrorMessage(), errorData.getThrowable()); } else if (LogLevel.INFO.equals(errorData.getLogLevel())) { log.info(errorData.getPreFormatedErrorMessage(), errorData.getThrowable()); } else if (LogLevel.DEBUG.equals(errorData.getLogLevel())) { log.debug(errorData.getPreFormatedErrorMessage(), errorData.getThrowable()); } else { log.warn("Get unsupported LogLevelType: {}. Use {} as default", errorData.getLogLevel(), LogLevel.ERROR); log.error(errorData.getPreFormatedErrorMessage(), errorData.getThrowable()); } } @Override public void forwardToErrorHandler(Pair errorToHandle, String errorKey, final HttpServletRequest req, final HttpServletResponse resp) throws GuiBuildException { final IGuiBuilderConfiguration parentHopGuiConfig = evaluateRequiredErrorHandlingMethod(errorToHandle .getFirst(), errorKey); if (parentHopGuiConfig != null) { log.trace("iFrame to parent hop requested. Building GUI step for error handling ... "); guiBuilder.build(req, resp, parentHopGuiConfig, "iFrame-to-parent"); } else { // build up redirect URL final String redirectUrl = generateErrorRedirectUrl(req, errorKey); resp.setContentType("text/html"); resp.setStatus(302); resp.addHeader("Location", redirectUrl); log.debug("REDIRECT TO: {}", redirectUrl); } } public void setGuiBuilder(final IGuiFormBuilder guiBuilder) { this.guiBuilder = guiBuilder; } /** * Finalize the requested protocol operation. * * @param req HttpServletRequest * @param resp HttpServletResponse * @param pendingReq Authentication request which is actually in process * @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)); } @PostConstruct private void initializer() { log.trace("Initializing {} ...", ProtocolAuthenticationService.class.getName()); } /** * Executes the requested protocol action. * * @param httpReq HttpServletRequest * @param httpResp HttpServletResponse * @param pendingReq 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 ClassCastException( "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 ClassNotFoundException( "Requested Auth. protocol processing Class is NULL or does not implement the IAction interface.", e); } } // private void writeHtmlErrorResponse(@NonNull final HttpServletRequest // httpReq, // @NonNull final HttpServletResponse httpResp, @NonNull final String msg, // @NonNull final String errorCode, // @Nullable final Object[] params, String externalErrorCode) throws // EaafException { // this.writeHtmlErrorResponse(httpReq, httpResp, msg, errorCode, params, // externalErrorCode, null, null); // } private void writeHtmlErrorResponse(@NonNull final HttpServletRequest httpReq, @NonNull final HttpServletResponse httpResp, @NonNull final String msg, @NonNull final String errorCode, @Nullable final Object[] params, String externalErrorCode, IErrorService.IHandleData errorData) throws 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) { /* replace all single-quotes by two single-quotes for escaping purposes to mitigate * Thymeleaf error in: * th:text="${#messages.msgWithParams('__${msg.errorCode}__', '__${msg.errorParams}__')}" */ errorCodeParams[i] = params[i].toString() .replaceAll("'", "''") .replaceAll("\\{", "{"); } else { errorCodeParams[i] = "null"; } } } // add errorcode and errormessage if (config instanceof ModifyableGuiBuilderConfiguration) { final ModifyableGuiBuilderConfiguration c = (ModifyableGuiBuilderConfiguration) config; c.putCustomParameter(AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERROMSG, msg); c.putCustomParameter(AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERRORCODE, errorCode); // TODO: should we keep the internal errorcode secret? c.putCustomParameter(AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_EXTERNAL_ERRORCODE, externalErrorCode); c.putCustomParameterWithOutEscaption(AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_ERRORCODEPARAMS, ArrayUtils.toString(errorCodeParams)); errorTicketService.displayErrorData(c, errorData, httpReq); } 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("internal.99", new Object[] {e.getMessage()}, e); } } private void displayException(final HttpServletRequest req, final HttpServletResponse resp, final IErrorService.IHandleData errorData) throws IOException, EaafException { final Throwable e = errorData.getThrowable(); final String internalErrorCode = errorData.getInternalErrorCode(); // send error response if (e instanceof ProtocolNotActiveException) { resp.getWriter().write(Encode.forHtml(e.getMessage())); resp.setContentType(EaafConstants.CONTENTTYPE_HTML_UTF8); resp.sendError(HttpServletResponse.SC_FORBIDDEN, StringEscapeUtils.escapeHtml4(StringEscapeUtils.escapeEcmaScript(e.getMessage()))); } else if (e instanceof AuthnRequestValidatorException || e instanceof InvalidProtocolRequestException || e instanceof ProcessExecutionException || e instanceof ConfigurationException) { // write error message writeHtmlErrorResponse(req, resp, e.getMessage(), internalErrorCode, e instanceof EaafException ? ((EaafException) e).getParams() : null, statusMessager.mapInternalErrorToExternalError(internalErrorCode), errorData); } else if (e instanceof EaafException) { // send HTML formated error message writeHtmlErrorResponse(req, resp, e.getMessage(), internalErrorCode, ((EaafException) e).getParams(), statusMessager.mapInternalErrorToExternalError(internalErrorCode), errorData); } else { // write generic message for general exceptions final String msg = statusMessager.getMessage(IStatusMessenger.CODES_INTERNAL_ERROR_GENERIC, null); writeHtmlErrorResponse(req, resp, msg, internalErrorCode, null, statusMessager.mapInternalErrorToExternalError(internalErrorCode), errorData); } } private IGuiBuilderConfiguration evaluateRequiredErrorHandlingMethod(IRequest first, String errorId) { if (first != null && first.isProcessInIframe()) { return guiConfigFactory .getDefaultIFrameParentHopGui(first, ProtocolFinalizationController.ENDPOINT_ERRORHANDLING, errorId); } return null; } private String generateErrorRedirectUrl(final HttpServletRequest req, String errorKey) { String redirectUrl = null; redirectUrl = ServletUtils.getBaseUrl(req); redirectUrl += ProtocolFinalizationController.ENDPOINT_ERRORHANDLING + "?" + EaafConstants.PARAM_HTTP_ERROR_CODE + "=" + errorKey; return redirectUrl; } }