001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nelson Silva <nelson.silva@inevo.pt>
018 */
019package org.nuxeo.ecm.platform.auth.saml;
020
021import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGIN_ERROR;
022
023import java.io.File;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.servlet.ServletException;
031import javax.servlet.http.Cookie;
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletResponse;
034import javax.servlet.http.HttpSession;
035
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.nuxeo.common.utils.i18n.I18NUtils;
040import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
041import org.nuxeo.ecm.platform.auth.saml.binding.HTTPPostBinding;
042import org.nuxeo.ecm.platform.auth.saml.binding.HTTPRedirectBinding;
043import org.nuxeo.ecm.platform.auth.saml.binding.SAMLBinding;
044import org.nuxeo.ecm.platform.auth.saml.key.KeyManager;
045import org.nuxeo.ecm.platform.auth.saml.slo.SLOProfile;
046import org.nuxeo.ecm.platform.auth.saml.slo.SLOProfileImpl;
047import org.nuxeo.ecm.platform.auth.saml.sso.WebSSOProfile;
048import org.nuxeo.ecm.platform.auth.saml.sso.WebSSOProfileImpl;
049import org.nuxeo.ecm.platform.auth.saml.user.AbstractUserResolver;
050import org.nuxeo.ecm.platform.auth.saml.user.EmailBasedUserResolver;
051import org.nuxeo.ecm.platform.auth.saml.user.UserMapperBasedResolver;
052import org.nuxeo.ecm.platform.auth.saml.user.UserResolver;
053import org.nuxeo.ecm.platform.ui.web.auth.LoginScreenHelper;
054import org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants;
055import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
056import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
057import org.nuxeo.ecm.platform.ui.web.auth.service.LoginProviderLinkComputer;
058import org.nuxeo.ecm.platform.web.common.CookieHelper;
059import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
060import org.nuxeo.runtime.api.Framework;
061import org.nuxeo.usermapper.service.UserMapperService;
062import org.opensaml.DefaultBootstrap;
063import org.opensaml.common.SAMLException;
064import org.opensaml.common.SAMLObject;
065import org.opensaml.common.binding.BasicSAMLMessageContext;
066import org.opensaml.common.binding.SAMLMessageContext;
067import org.opensaml.common.xml.SAMLConstants;
068import org.opensaml.saml2.core.AuthnRequest;
069import org.opensaml.saml2.core.LogoutRequest;
070import org.opensaml.saml2.core.LogoutResponse;
071import org.opensaml.saml2.core.NameID;
072import org.opensaml.saml2.encryption.Decrypter;
073import org.opensaml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
074import org.opensaml.saml2.metadata.EntityDescriptor;
075import org.opensaml.saml2.metadata.IDPSSODescriptor;
076import org.opensaml.saml2.metadata.RoleDescriptor;
077import org.opensaml.saml2.metadata.SPSSODescriptor;
078import org.opensaml.saml2.metadata.SingleLogoutService;
079import org.opensaml.saml2.metadata.SingleSignOnService;
080import org.opensaml.saml2.metadata.provider.AbstractMetadataProvider;
081import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
082import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
083import org.opensaml.saml2.metadata.provider.MetadataProvider;
084import org.opensaml.saml2.metadata.provider.MetadataProviderException;
085import org.opensaml.security.MetadataCredentialResolver;
086import org.opensaml.ws.message.decoder.MessageDecodingException;
087import org.opensaml.ws.transport.InTransport;
088import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
089import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
090import org.opensaml.xml.Configuration;
091import org.opensaml.xml.ConfigurationException;
092import org.opensaml.xml.encryption.ChainingEncryptedKeyResolver;
093import org.opensaml.xml.encryption.InlineEncryptedKeyResolver;
094import org.opensaml.xml.encryption.SimpleRetrievalMethodEncryptedKeyResolver;
095import org.opensaml.xml.parse.BasicParserPool;
096import org.opensaml.xml.security.credential.Credential;
097import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver;
098import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver;
099import org.opensaml.xml.signature.SignatureTrustEngine;
100import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine;
101
102/**
103 * A SAML2 authentication provider.
104 *
105 * @since 6.0
106 */
107public class SAMLAuthenticationProvider
108        implements NuxeoAuthenticationPlugin, LoginProviderLinkComputer, NuxeoAuthenticationPluginLogoutExtension {
109
110    private static final Log log = LogFactory.getLog(SAMLAuthenticationProvider.class);
111
112    private static final String ERROR_PAGE = "saml/error.jsp";
113
114    private static final String ERROR_AUTH = "error.saml.auth";
115
116    private static final String ERROR_USER = "error.saml.userMapping";
117
118    // User Resolver
119    private static final Class<? extends UserResolver> DEFAULT_USER_RESOLVER_CLASS = EmailBasedUserResolver.class;
120
121    private static final Class<? extends UserResolver> USERMAPPER_USER_RESOLVER_CLASS = UserMapperBasedResolver.class;
122
123    // SAML Constants
124    static final String SAML_SESSION_KEY = "SAML_SESSION";
125
126    // Supported SAML Bindings
127    // TODO: Allow registering new bindings
128    static List<SAMLBinding> bindings = new ArrayList<>();
129    static {
130        bindings.add(new HTTPPostBinding());
131        bindings.add(new HTTPRedirectBinding());
132    }
133
134    // Decryption key resolver
135    private static ChainingEncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver();
136    static {
137        encryptedKeyResolver.getResolverChain().add(new InlineEncryptedKeyResolver());
138        encryptedKeyResolver.getResolverChain().add(new EncryptedElementTypeEncryptedKeyResolver());
139        encryptedKeyResolver.getResolverChain().add(new SimpleRetrievalMethodEncryptedKeyResolver());
140    }
141
142    // Profiles supported by the IdP
143    private Map<String, AbstractSAMLProfile> profiles = new HashMap<>();
144
145    private UserResolver userResolver;
146
147    private KeyManager keyManager;
148
149    private SignatureTrustEngine trustEngine;
150
151    private Decrypter decrypter;
152
153    private MetadataProvider metadataProvider;
154
155    @Override
156    public void initPlugin(Map<String, String> parameters) {
157
158        // Initialize the User Resolver
159        String userResolverClassname = parameters.get("userResolverClass");
160        Class<? extends UserResolver> userResolverClass = null;
161        if (StringUtils.isBlank(userResolverClassname)) {
162            UserMapperService ums = Framework.getService(UserMapperService.class);
163            if (ums != null) {
164                userResolverClass = USERMAPPER_USER_RESOLVER_CLASS;
165            } else {
166                userResolverClass = DEFAULT_USER_RESOLVER_CLASS;
167            }
168        } else {
169            try {
170                userResolverClass = Class.forName(userResolverClassname).asSubclass(AbstractUserResolver.class);
171            } catch (ClassNotFoundException e) {
172                log.error("Failed get user resolver class " + userResolverClassname);
173            }
174
175        }
176        try {
177            userResolver = userResolverClass.newInstance();
178            userResolver.init(parameters);
179        } catch (InstantiationException | IllegalAccessException e) {
180            log.error("Failed to initialize user resolver " + userResolverClassname);
181        }
182
183        // Initialize the OpenSAML library
184        try {
185            DefaultBootstrap.bootstrap();
186        } catch (ConfigurationException e) {
187            log.error("Failed to bootstrap OpenSAML", e);
188        }
189
190        // Read the IdP metadata and initialize the supported profiles
191        try {
192            // Read the IdP metadata
193            initializeMetadataProvider(parameters);
194
195            // Setup Signature Trust Engine
196            MetadataCredentialResolver metadataCredentialResolver = new MetadataCredentialResolver(metadataProvider);
197            trustEngine = new ExplicitKeySignatureTrustEngine(metadataCredentialResolver,
198                    org.opensaml.xml.Configuration.getGlobalSecurityConfiguration()
199                                                  .getDefaultKeyInfoCredentialResolver());
200
201            // Setup decrypter
202            Credential encryptionCredential = getKeyManager().getEncryptionCredential();
203            if (encryptionCredential != null) {
204                KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(encryptionCredential);
205                decrypter = new Decrypter(null, resolver, encryptedKeyResolver);
206                decrypter.setRootInNewDocument(true);
207            }
208
209            // Process IdP roles
210            for (RoleDescriptor roleDescriptor : getIdPDescriptor().getRoleDescriptors()) {
211
212                // Web SSO role
213                if (roleDescriptor.getElementQName().equals(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)
214                        && roleDescriptor.isSupportedProtocol(org.opensaml.common.xml.SAMLConstants.SAML20P_NS)) {
215
216                    IDPSSODescriptor idpSSO = (IDPSSODescriptor) roleDescriptor;
217
218                    // SSO
219                    for (SingleSignOnService sso : idpSSO.getSingleSignOnServices()) {
220                        if (sso.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
221                            addProfile(new WebSSOProfileImpl(sso));
222                            break;
223                        }
224                    }
225
226                    // SLO
227                    for (SingleLogoutService slo : idpSSO.getSingleLogoutServices()) {
228                        if (slo.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
229                            addProfile(new SLOProfileImpl(slo));
230                            break;
231                        }
232                    }
233                }
234            }
235
236        } catch (MetadataProviderException e) {
237            log.warn("Failed to register IdP: " + e.getMessage());
238        }
239
240        // contribute icon and link to the Login Screen
241        if (StringUtils.isNotBlank(parameters.get("name"))) {
242            LoginScreenHelper.registerLoginProvider(parameters.get("name"), parameters.get("icon"), null,
243                    parameters.get("label"), parameters.get("description"), this);
244        }
245    }
246
247    private void addProfile(AbstractSAMLProfile profile) {
248        profile.setTrustEngine(trustEngine);
249        profile.setDecrypter(decrypter);
250        profiles.put(profile.getProfileIdentifier(), profile);
251    }
252
253    private void initializeMetadataProvider(Map<String, String> parameters) throws MetadataProviderException {
254        AbstractMetadataProvider metadataProvider;
255
256        String metadataUrl = parameters.get("metadata");
257        if (metadataUrl == null) {
258            throw new MetadataProviderException("No metadata URI set for provider "
259                    + ((parameters.containsKey("name")) ? parameters.get("name") : ""));
260        }
261
262        int requestTimeout = parameters.containsKey("timeout") ? Integer.parseInt(parameters.get("timeout")) : 5;
263
264        if (metadataUrl.startsWith("http:") || metadataUrl.startsWith("https:")) {
265            metadataProvider = new HTTPMetadataProvider(metadataUrl, requestTimeout * 1000);
266        } else { // file
267            metadataProvider = new FilesystemMetadataProvider(new File(metadataUrl));
268        }
269
270        metadataProvider.setParserPool(new BasicParserPool());
271        metadataProvider.initialize();
272
273        this.metadataProvider = metadataProvider;
274    }
275
276    private EntityDescriptor getIdPDescriptor() throws MetadataProviderException {
277        return (EntityDescriptor) metadataProvider.getMetadata();
278    }
279
280    /**
281     * Returns a Login URL to use with HTTP Redirect
282     */
283    protected String getSSOUrl(HttpServletRequest request, HttpServletResponse response) {
284        WebSSOProfile sso = (WebSSOProfile) profiles.get(WebSSOProfile.PROFILE_URI);
285        if (sso == null) {
286            return null;
287        }
288
289        // Create and populate the context
290        SAMLMessageContext context = new BasicSAMLMessageContext();
291        populateLocalContext(context, request);
292
293        // Store the requested URL in the Relay State
294        String requestedUrl = getRequestedUrl(request);
295        if (requestedUrl != null) {
296            context.setRelayState(requestedUrl);
297        }
298
299        // Build Uri
300        HTTPRedirectBinding binding = (HTTPRedirectBinding) getBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
301        String loginURL = sso.getEndpoint().getLocation();
302        try {
303            AuthnRequest authnRequest = sso.buildAuthRequest(request);
304            authnRequest.setDestination(sso.getEndpoint().getLocation());
305            context.setOutboundSAMLMessage(authnRequest);
306            loginURL = binding.buildRedirectURL(context, sso.getEndpoint().getLocation());
307        } catch (SAMLException e) {
308            log.error("Failed to build redirect URL", e);
309        }
310        return loginURL;
311    }
312
313    private String getRequestedUrl(HttpServletRequest request) {
314        String requestedUrl = (String) request.getAttribute(NXAuthConstants.REQUESTED_URL);
315        if (requestedUrl == null) {
316            HttpSession session = request.getSession(false);
317            if (session != null) {
318                requestedUrl = (String) session.getAttribute(NXAuthConstants.START_PAGE_SAVE_KEY);
319            }
320        }
321        return requestedUrl;
322    }
323
324    @Override
325    public String computeUrl(HttpServletRequest request, String requestedUrl) {
326        return getSSOUrl(request, null);
327    }
328
329    @Override
330    public Boolean handleLoginPrompt(HttpServletRequest request, HttpServletResponse response, String baseURL) {
331
332        String loginError = (String) request.getAttribute(LOGIN_ERROR);
333        if (loginError != null) {
334            try {
335                request.getRequestDispatcher(ERROR_PAGE).forward(request, response);
336                return true;
337            } catch (ServletException | IOException e) {
338                log.error("Failed to redirect to error page", e);
339                return false;
340            }
341        }
342
343        String loginURL = getSSOUrl(request, response);
344        try {
345            response.sendRedirect(loginURL);
346        } catch (IOException e) {
347            String errorMessage = String.format("Unable to send redirect on %s", loginURL);
348            log.error(errorMessage, e);
349            return false;
350        }
351        return true;
352    }
353
354    // Retrieves user identification information from the request.
355    @Override
356    public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest request, HttpServletResponse response) {
357
358        HttpServletRequestAdapter inTransport = new HttpServletRequestAdapter(request);
359        SAMLBinding binding = getBinding(inTransport);
360
361        // Check if we support this binding
362        if (binding == null) {
363            return null;
364        }
365
366        HttpServletResponseAdapter outTransport = new HttpServletResponseAdapter(response, request.isSecure());
367
368        // Create and populate the context
369        SAMLMessageContext context = new BasicSAMLMessageContext();
370        context.setInboundMessageTransport(inTransport);
371        context.setOutboundMessageTransport(outTransport);
372        populateLocalContext(context, request);
373
374        // Decode the message
375        try {
376            binding.decode(context);
377        } catch (org.opensaml.xml.security.SecurityException | MessageDecodingException e) {
378            log.error("Error during SAML decoding", e);
379            return null;
380        }
381
382        // Set Peer context info if needed
383        try {
384            if (context.getPeerEntityId() == null) {
385                context.setPeerEntityId(getIdPDescriptor().getEntityID());
386            }
387            if (context.getPeerEntityMetadata() == null) {
388                context.setPeerEntityMetadata(getIdPDescriptor());
389            }
390            if (context.getPeerEntityRole() == null) {
391                context.setPeerEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
392            }
393        } catch (MetadataProviderException e) {
394            //
395        }
396
397        // Check for a response processor for this profile
398        AbstractSAMLProfile processor = getProcessor(context);
399
400        if (processor == null) {
401            log.warn("Unsupported profile encountered in the context " + context.getCommunicationProfileId());
402            return null;
403        }
404
405        // Set the communication profile
406        context.setCommunicationProfileId(processor.getProfileIdentifier());
407
408        // Delegate handling the message to the processor
409        SAMLObject message = context.getInboundSAMLMessage();
410
411        // Handle SLO
412        // TODO - Try to handle IdP initiated SLO somewhere else
413        if (processor instanceof SLOProfile) {
414            SLOProfile slo = (SLOProfile) processor;
415            try {
416                // Handle SLO response
417                if (message instanceof LogoutResponse) {
418                    slo.processLogoutResponse(context);
419                    // Handle SLO request
420                } else if (message instanceof LogoutRequest) {
421                    SAMLCredential credential = getSamlCredential(request);
422                    slo.processLogoutRequest(context, credential);
423                }
424            } catch (SAMLException e) {
425                log.debug("Error processing SAML message", e);
426            }
427            return null;
428        }
429
430        // Handle SSO
431        SAMLCredential credential;
432
433        try {
434            credential = ((WebSSOProfile) processor).processAuthenticationResponse(context);
435        } catch (SAMLException e) {
436            log.error("Error processing SAML message", e);
437            sendError(request, ERROR_AUTH);
438            return null;
439        }
440
441        String userId = Framework.doPrivileged(() -> userResolver.findOrCreateNuxeoUser(credential));
442        if (userId == null) {
443            log.warn("Failed to resolve user with NameID \"" + credential.getNameID().getValue() + "\".");
444            sendError(request, ERROR_USER);
445            return null;
446        }
447
448        // Store session id in a cookie
449        if (credential.getSessionIndexes() != null && !credential.getSessionIndexes().isEmpty()) {
450            String nameValue = credential.getNameID().getValue();
451            String nameFormat = credential.getNameID().getFormat();
452            String sessionId = credential.getSessionIndexes().get(0);
453            Cookie cookie = CookieHelper.createCookie(request, SAML_SESSION_KEY,
454                    String.join("|", sessionId, nameValue, nameFormat));
455            response.addCookie(cookie);
456        }
457
458        // Redirect to URL in relay state if any
459        HttpSession session = request.getSession(!response.isCommitted());
460        if (session != null) {
461            if (StringUtils.isNotEmpty(credential.getRelayState())) {
462                session.setAttribute(NXAuthConstants.START_PAGE_SAVE_KEY, credential.getRelayState());
463            }
464        }
465
466        return new UserIdentificationInfo(userId, userId);
467    }
468
469    protected AbstractSAMLProfile getProcessor(SAMLMessageContext context) {
470        String profileId;
471        SAMLObject message = context.getInboundSAMLMessage();
472        if (message instanceof LogoutResponse || message instanceof LogoutRequest) {
473            profileId = SLOProfile.PROFILE_URI;
474        } else {
475            profileId = WebSSOProfile.PROFILE_URI;
476        }
477
478        return profiles.get(profileId);
479    }
480
481    protected SAMLBinding getBinding(String bindingURI) {
482        for (SAMLBinding binding : bindings) {
483            if (binding.getBindingURI().equals(bindingURI)) {
484                return binding;
485            }
486        }
487        return null;
488    }
489
490    protected SAMLBinding getBinding(InTransport transport) {
491        for (SAMLBinding binding : bindings) {
492            if (binding.supports(transport)) {
493                return binding;
494            }
495        }
496        return null;
497    }
498
499    private void populateLocalContext(SAMLMessageContext context, HttpServletRequest request) {
500        // Set local info
501        context.setLocalEntityId(SAMLConfiguration.getEntityId());
502        context.setLocalEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
503
504        // Set local entity role metadata
505        String baseURL = VirtualHostHelper.getBaseURL(request);
506        baseURL += (baseURL.endsWith("/") ? "" : "/") + LoginScreenHelper.getStartupPagePath();
507        SPSSODescriptor descriptor = SAMLConfiguration.getSPSSODescriptor(baseURL);
508        context.setLocalEntityRoleMetadata(descriptor);
509
510        context.setMetadataProvider(metadataProvider);
511
512        // Set the signing key
513        keyManager = Framework.getService(KeyManager.class);
514        if (getKeyManager().getSigningCredential() != null) {
515            context.setOutboundSAMLMessageSigningCredential(getKeyManager().getSigningCredential());
516        }
517    }
518
519    @Override
520    public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
521        return true;
522    }
523
524    @Override
525    public List<String> getUnAuthenticatedURLPrefix() {
526        return null;
527    }
528
529    /**
530     * Returns a Logout URL to use with HTTP Redirect
531     */
532    protected String getSLOUrl(HttpServletRequest request, HttpServletResponse response) {
533        SLOProfile slo = (SLOProfile) profiles.get(SLOProfile.PROFILE_URI);
534        if (slo == null) {
535            return null;
536        }
537
538        String logoutURL = slo.getEndpoint().getLocation();
539
540        SAMLCredential credential = getSamlCredential(request);
541
542        // Create and populate the context
543        SAMLMessageContext context = new BasicSAMLMessageContext();
544        populateLocalContext(context, request);
545
546        try {
547            LogoutRequest logoutRequest = slo.buildLogoutRequest(context, credential);
548            logoutRequest.setDestination(slo.getEndpoint().getLocation());
549            context.setOutboundSAMLMessage(logoutRequest);
550
551            HTTPRedirectBinding binding = (HTTPRedirectBinding) getBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
552            logoutURL = binding.buildRedirectURL(context, slo.getEndpoint().getLocation());
553        } catch (SAMLException e) {
554            log.error("Failed to get SAML Logout request", e);
555        }
556
557        return logoutURL;
558    }
559
560    private SAMLCredential getSamlCredential(HttpServletRequest request) {
561        SAMLCredential credential = null;
562
563        // Retrieve the SAMLCredential credential from cookie
564        Cookie cookie = getCookie(request, SAML_SESSION_KEY);
565        if (cookie != null) {
566            String[] parts = cookie.getValue().split("\\|");
567            String sessionId = parts[0];
568            String nameValue = parts[1];
569            String nameFormat = parts[2];
570
571            NameID nameID = (NameID) Configuration.getBuilderFactory()
572                                                  .getBuilder(NameID.DEFAULT_ELEMENT_NAME)
573                                                  .buildObject(NameID.DEFAULT_ELEMENT_NAME);
574            nameID.setValue(nameValue);
575            nameID.setFormat(nameFormat);
576
577            List<String> sessionIndexes = new ArrayList<>();
578            sessionIndexes.add(sessionId);
579
580            credential = new SAMLCredential(nameID, sessionIndexes);
581        }
582
583        return credential;
584    }
585
586    @Override
587    public Boolean handleLogout(HttpServletRequest request, HttpServletResponse response) {
588        String logoutURL = getSLOUrl(request, response);
589
590        if (logoutURL == null) {
591            return false;
592        }
593
594        if (log.isDebugEnabled()) {
595            log.debug("Send redirect to " + logoutURL);
596        }
597
598        try {
599            response.sendRedirect(logoutURL);
600        } catch (IOException e) {
601            String errorMessage = String.format("Unable to send redirect on %s", logoutURL);
602            log.error(errorMessage, e);
603            return false;
604        }
605
606        Cookie cookie = getCookie(request, SAML_SESSION_KEY);
607        if (cookie != null) {
608            removeCookie(response, cookie);
609        }
610
611        return true;
612    }
613
614    private void sendError(HttpServletRequest req, String key) {
615        String msg = I18NUtils.getMessageString("messages", key, null, req.getLocale());
616        req.setAttribute(LOGIN_ERROR, msg);
617    }
618
619    private KeyManager getKeyManager() {
620        if (keyManager == null) {
621            keyManager = Framework.getService(KeyManager.class);
622        }
623        return keyManager;
624    }
625
626    private Cookie getCookie(HttpServletRequest httpRequest, String cookieName) {
627        Cookie cookies[] = httpRequest.getCookies();
628        if (cookies != null) {
629            for (Cookie cooky : cookies) {
630                if (cookieName.equals(cooky.getName())) {
631                    return cooky;
632                }
633            }
634        }
635        return null;
636    }
637
638    private void removeCookie(HttpServletResponse httpResponse, Cookie cookie) {
639        log.debug(String.format("Removing cookie %s.", cookie.getName()));
640        cookie.setMaxAge(0);
641        cookie.setValue("");
642        httpResponse.addCookie(cookie);
643    }
644}