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