/******************************************************************************* * Copyright 2014 Federal Chancellery Austria * MOA-ID has been developed in a cooperation between BRZ, the Federal * Chancellery Austria - ICT staff unit, and Graz University of Technology. * * Licensed under the EUPL, Version 1.1 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: * http://www.osor.eu/eupl/ * * 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.egovernment.moa.id.moduls; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringEscapeUtils; import org.apache.velocity.VelocityContext; import org.opensaml.saml2.core.LogoutRequest; import org.opensaml.saml2.core.LogoutResponse; import org.opensaml.saml2.core.StatusCode; import org.opensaml.saml2.metadata.SingleLogoutService; import org.opensaml.ws.soap.common.SOAPException; import org.opensaml.xml.XMLObject; import org.opensaml.xml.security.SecurityException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import at.gv.egovernment.moa.id.advancedlogging.MOAIDEventConstants; import at.gv.egovernment.moa.id.advancedlogging.MOAReversionLogger; import at.gv.egovernment.moa.id.auth.MOAIDAuthConstants; import at.gv.egovernment.moa.id.auth.data.AuthenticationSession; import at.gv.egovernment.moa.id.auth.data.AuthenticationSessionExtensions; import at.gv.egovernment.moa.id.auth.exception.InvalidProtocolRequestException; import at.gv.egovernment.moa.id.auth.exception.MOAIDException; import at.gv.egovernment.moa.id.auth.modules.SingleSignOnConsentsModuleImpl; import at.gv.egovernment.moa.id.auth.modules.TaskExecutionException; import at.gv.egovernment.moa.id.auth.modules.registration.ModuleRegistration; import at.gv.egovernment.moa.id.commons.db.dao.session.InterfederationSessionStore; import at.gv.egovernment.moa.id.commons.db.dao.session.OASessionStore; import at.gv.egovernment.moa.id.commons.db.ex.MOADatabaseException; import at.gv.egovernment.moa.id.config.auth.AuthConfiguration; import at.gv.egovernment.moa.id.config.auth.IOAAuthParameters; import at.gv.egovernment.moa.id.data.SLOInformationContainer; import at.gv.egovernment.moa.id.data.SLOInformationImpl; import at.gv.egovernment.moa.id.process.ExecutionContextImpl; import at.gv.egovernment.moa.id.process.ProcessEngine; import at.gv.egovernment.moa.id.process.ProcessExecutionException; import at.gv.egovernment.moa.id.process.api.ExecutionContext; import at.gv.egovernment.moa.id.protocols.pvp2x.PVPTargetConfiguration; import at.gv.egovernment.moa.id.protocols.pvp2x.builder.SingleLogOutBuilder; import at.gv.egovernment.moa.id.protocols.pvp2x.messages.MOARequest; import at.gv.egovernment.moa.id.protocols.pvp2x.utils.MOASAMLSOAPClient; import at.gv.egovernment.moa.id.protocols.pvp2x.verification.SAMLVerificationEngine; import at.gv.egovernment.moa.id.protocols.pvp2x.verification.TrustEngineFactory; import at.gv.egovernment.moa.id.storage.IAuthenticationSessionStoreage; import at.gv.egovernment.moa.id.storage.ITransactionStorage; import at.gv.egovernment.moa.id.util.MOAIDMessageProvider; import at.gv.egovernment.moa.id.util.ParamValidatorUtils; import at.gv.egovernment.moa.id.util.Random; import at.gv.egovernment.moa.id.util.legacy.LegacyHelper; import at.gv.egovernment.moa.logging.Logger; import at.gv.egovernment.moa.util.MiscUtil; @Service("MOAID_AuthenticationManager") public class AuthenticationManager extends MOAIDAuthConstants { public static final String MOA_SESSION = "MoaAuthenticationSession"; public static final String MOA_AUTHENTICATED = "MoaAuthenticated"; public static final int SLOTIMEOUT = 30 * 1000; //30 sec @Autowired private ProcessEngine processEngine; @Autowired private SSOManager ssoManager; @Autowired private IRequestStorage requestStoreage; @Autowired private ITransactionStorage transactionStorage; @Autowired private IAuthenticationSessionStoreage authenticatedSessionStore; @Autowired private MOAReversionLogger revisionsLogger; @Autowired protected AuthConfiguration authConfig; @Autowired private SingleLogOutBuilder sloBuilder; @Autowired private SAMLVerificationEngine samlVerificationEngine; public void performSingleLogOut(HttpServletRequest httpReq, HttpServletResponse httpResp, AuthenticationSession session, PVPTargetConfiguration pvpReq) throws MOAIDException { performSingleLogOut(httpReq, httpResp, session, pvpReq, null); } public void performSingleLogOut(HttpServletRequest httpReq, HttpServletResponse httpResp, AuthenticationSession session, String authURL) throws MOAIDException { performSingleLogOut(httpReq, httpResp, session, null, authURL); } public void performOnlyIDPLogOut(HttpServletRequest request, HttpServletResponse response, String moaSessionID) { Logger.info("Logout"); if(moaSessionID == null) { moaSessionID = (String) request.getParameter(PARAM_SESSIONID); } if(moaSessionID == null) { Logger.info("NO MOA Session to logout"); return; } AuthenticationSession authSession; try { authSession = authenticatedSessionStore.getSession(moaSessionID); if(authSession == null) { Logger.info("NO MOA Authentication data for ID " + moaSessionID); return; } authSession.setAuthenticated(false); //HTTPSessionUtils.setHTTPSessionString(session, MOA_SESSION, null); // remove moa session from HTTP Session //log Session_Destroy to reversionslog AuthenticationSessionExtensions sessionExtensions = authenticatedSessionStore.getAuthenticationSessionExtensions(moaSessionID); revisionsLogger.logEvent(MOAIDEventConstants.SESSION_DESTROYED, sessionExtensions.getUniqueSessionId()); authenticatedSessionStore.destroySession(moaSessionID); //session.invalidate(); } catch (MOADatabaseException e) { Logger.info("NO MOA Authentication data for ID " + moaSessionID); return; } } /** * Authenticates the authentication request {pendingReq}, which is actually processed * * @param httpReq HttpServletRequest * @param httpResp HttpServletResponse * @param protocolRequest Authentication request which is actually in process * * @return Return already authenticated MOASession if exists, otherwise return null * @throws MOADatabaseException * @throws MOAIDException * @throws IOException * @throws ServletException * */ public AuthenticationSession doAuthentication(HttpServletRequest httpReq, HttpServletResponse httpResp, RequestImpl pendingReq) throws MOADatabaseException, ServletException, IOException, MOAIDException { //generic authentication request validation if (pendingReq.isPassiv() && pendingReq.forceAuth()) { // conflict! throw new NoPassivAuthenticationException(); } //get SSO cookie from http request String ssoId = ssoManager.getSSOSessionID(httpReq); //check if interfederation IDP is requested ssoManager.checkInterfederationIsRequested(httpReq, httpResp, pendingReq); //check if SSO session cookie is already used if (ssoId != null) { String correspondingMOASession = ssoManager.existsOldSSOSession(ssoId); if (correspondingMOASession != null) { Logger.warn("Request sends an old SSO Session ID("+ssoId+")! " + "Invalidate the corresponding MOASession with ID="+ correspondingMOASession); revisionsLogger.logEvent(pendingReq.getOnlineApplicationConfiguration(), pendingReq, MOAIDEventConstants.AUTHPROCESS_SSO_INVALID); authenticatedSessionStore.destroySession(correspondingMOASession); ssoManager.deleteSSOSessionID(httpReq, httpResp); } } //check if SSO Session is valid boolean isValidSSOSession = ssoManager.isValidSSOSession(ssoId, pendingReq); // check if Service-Provider allows SSO sessions IOAAuthParameters oaParam = pendingReq.getOnlineApplicationConfiguration(); boolean useSSOOA = oaParam.useSSO() || oaParam.isInderfederationIDP(); revisionsLogger.logEvent(oaParam, pendingReq, MOAIDEventConstants.AUTHPROCESS_SERVICEPROVIDER, pendingReq.getOAURL()); //if a legacy request is used SSO should not be allowed in case of mandate authentication boolean isUseMandateRequested = LegacyHelper.isUseMandateRequested(httpReq); //check if SSO is allowed for the actually executed request //INFO: Actually, useMandate disables SSO functionality!!!!! boolean isSSOAllowed = (useSSOOA && !isUseMandateRequested); pendingReq.setNeedSingleSignOnFunctionality(isSSOAllowed); //get MOASession from SSO-Cookie if SSO is allowed AuthenticationSession moaSession = null; if (isValidSSOSession && isSSOAllowed) { String moasessionID = ssoManager.getMOASession(ssoId); moaSession = authenticatedSessionStore.getSession(moasessionID); if (moaSession == null) Logger.info("No MOASession FOUND with provided SSO-Cookie."); else { Logger.debug("Found authenticated MOASession with provided SSO-Cookie."); revisionsLogger.logEvent(oaParam, pendingReq, MOAIDEventConstants.AUTHPROCESS_SSO); } } //check if session is already authenticated boolean isSessionAuthenticated = tryPerformAuthentication((RequestImpl) pendingReq, moaSession); //force new authentication authentication process if (pendingReq.forceAuth()) { startAuthenticationProcess(httpReq, httpResp, pendingReq); return null; //perform SSO-Consents evaluation if it it required } else if (isSessionAuthenticated && oaParam.useSSOQuestion()) { sendSingleSignOnConsentsEvaluation(httpReq, httpResp, pendingReq); return null; } else if (pendingReq.isPassiv()) { if (isSessionAuthenticated) { // Passive authentication ok! revisionsLogger.logEvent(oaParam, pendingReq, MOAIDEventConstants.AUTHPROCESS_FINISHED); return moaSession; } else { throw new NoPassivAuthenticationException(); } } else { if (isSessionAuthenticated) { // Is authenticated .. proceed revisionsLogger.logEvent(oaParam, pendingReq, MOAIDEventConstants.AUTHPROCESS_FINISHED); return moaSession; } else { // Start authentication! startAuthenticationProcess(httpReq, httpResp, pendingReq); return null; } } } /** * Checks if a authenticated MOASession already exists and if {protocolRequest} is authenticated * * @param protocolRequest Authentication request which is actually in process * @param moaSession MOASession with authentication information or null if no active MOASession exists * * @return true if session is already authenticated, otherwise false * @throws MOAIDException */ private boolean tryPerformAuthentication(RequestImpl protocolRequest, AuthenticationSession moaSession) { //if no MOASession exist -> authentication is required if (moaSession == null) { return false; } else { //if MOASession is Found but not authenticated --> authentication is required if (!moaSession.isAuthenticated()) { return false; } //if MOASession is already authenticated and protocol-request is authenticated // --> no authentication is required any more else if (moaSession.isAuthenticated() && protocolRequest.isAuthenticated()) { return true; // if MOASession is authenticated and SSO is allowed --> authenticate pendingRequest } else if (!protocolRequest.isAuthenticated() && moaSession.isAuthenticated() && protocolRequest.needSingleSignOnFunctionality()) { Logger.debug("Found active MOASession and SSO is allowed --> pendingRequest is authenticted"); protocolRequest.setAuthenticated(true); protocolRequest.setMOASessionIdentifier(moaSession.getSessionID()); return true; } // force authentication as backup solution else { Logger.warn("Authentication-required check find an unsuspected state --> force authentication"); return false; } } } private void startAuthenticationProcess(HttpServletRequest httpReq, HttpServletResponse httpResp, RequestImpl pendingReq) throws ServletException, IOException, MOAIDException { Logger.info("Starting authentication ..."); revisionsLogger.logEvent(pendingReq.getOnlineApplicationConfiguration(), pendingReq, MOAIDEventConstants.AUTHPROCESS_START); //is legacy allowed List legacyallowed_prot = authConfig.getLegacyAllowedProtocols(); boolean legacyallowed = legacyallowed_prot.contains(pendingReq.requestedModule()); //check legacy request parameter boolean legacyparamavail = ParamValidatorUtils.areAllLegacyParametersAvailable(httpReq); //create MOASession object AuthenticationSession moasession; try { moasession = authenticatedSessionStore.createSession(pendingReq); pendingReq.setMOASessionIdentifier(moasession.getSessionID()); } catch (MOADatabaseException e1) { Logger.error("Database Error! MOASession can not be created!"); throw new MOAIDException("init.04", new Object[] {}); } //create authentication process execution context ExecutionContext executionContext = new ExecutionContextImpl(); //set interfederation authentication flag executionContext.put(MOAIDAuthConstants.PROCESSCONTEXT_PERFORM_INTERFEDERATION_AUTH, MiscUtil.isNotEmpty( pendingReq.getGenericData(RequestImpl.DATAID_INTERFEDERATIOIDP_URL, String.class))); //set legacy mode or BKU-selection flags boolean leagacyMode = (legacyallowed && legacyparamavail); executionContext.put(MOAIDAuthConstants.PROCESSCONTEXT_ISLEGACYREQUEST, leagacyMode); executionContext.put(MOAIDAuthConstants.PROCESSCONTEXT_PERFORM_BKUSELECTION, !leagacyMode && MiscUtil.isEmpty(pendingReq.getGenericData(RequestImpl.DATAID_INTERFEDERATIOIDP_URL, String.class))); //add leagcy parameters to context if (leagacyMode) { Enumeration reqParamNames = httpReq.getParameterNames(); while(reqParamNames.hasMoreElements()) { String paramName = reqParamNames.nextElement(); if (MiscUtil.isNotEmpty(paramName) && MOAIDAuthConstants.LEGACYPARAMETERWHITELIST.contains(paramName)) executionContext.put(paramName, StringEscapeUtils.escapeHtml(httpReq.getParameter(paramName))); } } //start process engine startProcessEngine(pendingReq, executionContext); } private void sendSingleSignOnConsentsEvaluation(HttpServletRequest request, HttpServletResponse response, RequestImpl pendingReq) throws ServletException, IOException, MOAIDException { Logger.info("Start SSO user-consents evaluation ..."); //set authenticated flag to false, because user consents is required pendingReq.setAuthenticated(false); //create execution context ExecutionContext executionContext = new ExecutionContextImpl(); executionContext.put(SingleSignOnConsentsModuleImpl.PARAM_SSO_CONSENTS_EVALUATION, true); //start process engine startProcessEngine(pendingReq, executionContext); } private void startProcessEngine(RequestImpl pendingReq, ExecutionContext executionContext) throws MOAIDException { try { //put pending-request ID on execurtionContext executionContext.put(MOAIDAuthConstants.PARAM_TARGET_PENDINGREQUESTID, pendingReq.getRequestID()); // create process instance String processDefinitionId = ModuleRegistration.getInstance().selectProcess(executionContext); if (processDefinitionId == null) { Logger.warn("No suitable process found for SessionID " + pendingReq.getRequestID() ); throw new MOAIDException("process.02",new Object[] { pendingReq.getRequestID()}); } String processInstanceId = processEngine.createProcessInstance(processDefinitionId, executionContext); // keep process instance id in protocol pending-request pendingReq.setProcessInstanceId(processInstanceId); //store pending-request requestStoreage.storePendingRequest(pendingReq); // start process processEngine.start(pendingReq); } catch (ProcessExecutionException e) { Throwable cause = e.getCause(); if (cause != null && cause instanceof TaskExecutionException) { Throwable taskCause = cause.getCause(); if (taskCause != null && taskCause instanceof MOAIDException) { MOAIDException moaTaskCause = (MOAIDException) taskCause; Logger.warn(taskCause); throw moaTaskCause; } } throw new MOAIDException("process.01", new Object[] { pendingReq.getProcessInstanceId(), pendingReq.getRequestID() }, e); } } private void performSingleLogOut(HttpServletRequest httpReq, HttpServletResponse httpResp, AuthenticationSession session, PVPTargetConfiguration pvpReq, String authURL) throws MOAIDException { String pvpSLOIssuer = null; String inboundRelayState = null; if (pvpReq != null) { MOARequest samlReq = (MOARequest) pvpReq.getRequest(); LogoutRequest logOutReq = (LogoutRequest) samlReq.getSamlRequest(); pvpSLOIssuer = logOutReq.getIssuer().getValue(); inboundRelayState = samlReq.getRelayState(); } //store active OAs to SLOContaine List dbOAs = authenticatedSessionStore.getAllActiveOAFromMOASession(session); List dbIDPs = authenticatedSessionStore.getAllActiveIDPsFromMOASession(session); SLOInformationContainer sloContainer = new SLOInformationContainer(); sloContainer.setSloRequest(pvpReq); sloBuilder.parseActiveIDPs(sloContainer, dbIDPs, pvpSLOIssuer); sloBuilder.parseActiveOAs(sloContainer, dbOAs, pvpSLOIssuer); //terminate MOASession try { authenticatedSessionStore.destroySession(session.getSessionID()); ssoManager.deleteSSOSessionID(httpReq, httpResp); } catch (MOADatabaseException e) { Logger.warn("Delete MOASession FAILED."); sloContainer.putFailedOA(pvpReq.getAuthURL()); } //start service provider back channel logout process Iterator nextOAInterator = sloContainer.getNextBackChannelOA(); while (nextOAInterator.hasNext()) { SLOInformationImpl sloDescr = sloContainer.getBackChannelOASessionDescripten(nextOAInterator.next()); LogoutRequest sloReq = sloBuilder.buildSLORequestMessage(sloDescr); try { List soapResp = MOASAMLSOAPClient.send(sloDescr.getServiceURL(), sloReq); LogoutResponse sloResp = null; for (XMLObject el : soapResp) { if (el instanceof LogoutResponse) sloResp = (LogoutResponse) el; } if (sloResp == null) { Logger.warn("Single LogOut for OA " + sloReq.getIssuer().getValue() + " FAILED. NO LogOut response received."); sloContainer.putFailedOA(sloReq.getIssuer().getValue()); } else { samlVerificationEngine.verifySLOResponse(sloResp, TrustEngineFactory.getSignatureKnownKeysTrustEngine()); } sloBuilder.checkStatusCode(sloContainer, sloResp); } catch (SOAPException e) { Logger.warn("Single LogOut for OA " + sloReq.getIssuer().getValue() + " FAILED.", e); sloContainer.putFailedOA(sloReq.getIssuer().getValue()); } catch (SecurityException | InvalidProtocolRequestException e) { Logger.warn("Single LogOut for OA " + sloReq.getIssuer().getValue() + " FAILED.", e); sloContainer.putFailedOA(sloReq.getIssuer().getValue()); } } //start service provider front channel logout process try { if (sloContainer.hasFrontChannelOA()) { String relayState = Random.nextRandom(); Collection> sloDescr = sloContainer.getFrontChannelOASessionDescriptions(); List sloReqList = new ArrayList(); for (Entry el : sloDescr) { LogoutRequest sloReq = sloBuilder.buildSLORequestMessage(el.getValue()); try { sloReqList.add(sloBuilder.getFrontChannelSLOMessageURL(el.getValue().getServiceURL(), el.getValue().getBinding(), sloReq, httpReq, httpResp, relayState)); } catch (Exception e) { Logger.warn("Failed to build SLO request for OA:" + el.getKey()); sloContainer.putFailedOA(el.getKey()); } } //put SLO process-information into transaction storage transactionStorage.put(relayState, sloContainer); if (MiscUtil.isEmpty(authURL)) authURL = pvpReq.getAuthURL(); String timeOutURL = authURL + "/idpSingleLogout" + "?restart=" + relayState; VelocityContext context = new VelocityContext(); context.put("redirectURLs", sloReqList); context.put("timeoutURL", timeOutURL); context.put("timeout", SLOTIMEOUT); ssoManager.printSingleLogOutInfo(context, httpResp); } else { if (pvpReq != null) { //send SLO response to SLO request issuer SingleLogoutService sloService = sloBuilder.getResponseSLODescriptor(pvpReq); LogoutResponse message = sloBuilder.buildSLOResponseMessage(sloService, pvpReq, sloContainer.getSloFailedOAs()); sloBuilder.sendFrontChannelSLOMessage(sloService, message, httpReq, httpResp, inboundRelayState); } else { //print SLO information directly VelocityContext context = new VelocityContext(); if (sloContainer.getSloFailedOAs() == null || sloContainer.getSloFailedOAs().size() == 0) context.put("successMsg", MOAIDMessageProvider.getInstance().getMessage("slo.00", null)); else context.put("errorMsg", MOAIDMessageProvider.getInstance().getMessage("slo.01", null)); ssoManager.printSingleLogOutInfo(context, httpResp); } } } catch (MOADatabaseException e) { Logger.error("MOA AssertionDatabase ERROR", e); if (pvpReq != null) { SingleLogoutService sloService = sloBuilder.getResponseSLODescriptor(pvpReq); LogoutResponse message = sloBuilder.buildSLOErrorResponse(sloService, pvpReq, StatusCode.RESPONDER_URI); sloBuilder.sendFrontChannelSLOMessage(sloService, message, httpReq, httpResp, inboundRelayState); }else { //print SLO information directly VelocityContext context = new VelocityContext(); context.put("errorMsg", MOAIDMessageProvider.getInstance().getMessage("slo.01", null)); ssoManager.printSingleLogOutInfo(context, httpResp); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }