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