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