001/*
002 * (C) Copyright 2006-2009 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.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 *     Nuxeo - initial API and implementation
016 *     Academie de Rennes - proxy CAS support
017 *
018 * $Id: JOOoConvertPluginImpl.java 18651 2007-05-13 20:28:53Z sfermigier $
019 */
020
021package org.nuxeo.ecm.platform.ui.web.auth.cas2;
022
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.servlet.http.Cookie;
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032import javax.xml.parsers.ParserConfigurationException;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.common.utils.URIUtils;
037import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
038import org.nuxeo.ecm.platform.ui.web.auth.interfaces.LoginResponseHandler;
039import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
040import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
041import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
042import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
043import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
044import org.nuxeo.runtime.api.Framework;
045import org.xml.sax.SAXException;
046
047import edu.yale.its.tp.cas.client.ProxyTicketValidator;
048import edu.yale.its.tp.cas.client.ServiceTicketValidator;
049
050/**
051 * @author Thierry Delprat
052 * @author Olivier Adam
053 * @author M.-A. Darche
054 * @author Benjamin Jalon
055 * @author Thierry Martins
056 */
057public class Cas2Authenticator implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension,
058        LoginResponseHandler {
059
060    protected static final String CAS_SERVER_HEADER_KEY = "CasServer";
061
062    protected static final String CAS_SERVER_PATTERN_KEY = "$CASSERVER";
063
064    protected static final String NUXEO_SERVER_PATTERN_KEY = "$NUXEO";
065
066    protected static final String LOGIN_ACTION = "Login";
067
068    protected static final String LOGOUT_ACTION = "Logout";
069
070    protected static final String VALIDATE_ACTION = "Valid";
071
072    protected static final String PROXY_VALIDATE_ACTION = "ProxyValid";
073
074    protected static final Log log = LogFactory.getLog(Cas2Authenticator.class);
075
076    protected static final String EXCLUDE_PROMPT_KEY = "excludePromptURL";
077
078    protected static final String ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME = "org.nuxeo.auth.plugin.alternative";
079
080    protected String ticketKey = "ticket";
081
082    protected String proxyKey = "proxy";
083
084    protected String appURL = "http://127.0.0.1:8080/nuxeo/";
085
086    protected String serviceLoginURL = "http://127.0.0.1:8080/cas/login";
087
088    protected String serviceValidateURL = "http://127.0.0.1:8080/cas/serviceValidate";
089
090    /**
091     * We tell the CAS server whether we want a plain text (CAS 1.0) or XML (CAS 2.0) response by making the request
092     * either to the '.../validate' or '.../serviceValidate' URL. The older protocol supports only the CAS 1.0
093     * functionality, which is left around as the legacy '.../validate' URL.
094     */
095    protected String proxyValidateURL = "http://127.0.0.1:8080/cas/proxyValidate";
096
097    protected String serviceKey = "service";
098
099    protected String logoutURL = "";
100
101    protected String defaultCasServer = "";
102
103    protected String ticketValidatorClassName = "edu.yale.its.tp.cas.client.ServiceTicketValidator";
104
105    protected String proxyValidatorClassName = "edu.yale.its.tp.cas.client.ProxyTicketValidator";
106
107    protected boolean promptLogin = true;
108
109    protected List<String> excludePromptURLs;
110
111    protected String errorPage;
112
113    public List<String> getUnAuthenticatedURLPrefix() {
114        // CAS login screen is not part of Nuxeo5 Web App
115        return null;
116    }
117
118    protected String getServiceURL(HttpServletRequest httpRequest, String action) {
119        String url = "";
120        if (action.equals(LOGIN_ACTION)) {
121            url = serviceLoginURL;
122        } else if (action.equals(LOGOUT_ACTION)) {
123            url = logoutURL;
124        } else if (action.equals(VALIDATE_ACTION)) {
125            url = serviceValidateURL;
126        } else if (action.equals(PROXY_VALIDATE_ACTION)) {
127            url = proxyValidateURL;
128        }
129
130        if (url.contains(CAS_SERVER_PATTERN_KEY)) {
131            String serverURL = httpRequest.getHeader(CAS_SERVER_HEADER_KEY);
132            if (serverURL != null) {
133                url = url.replace(CAS_SERVER_PATTERN_KEY, serverURL);
134            } else {
135                if (url.contains(CAS_SERVER_PATTERN_KEY)) {
136                    url = url.replace(CAS_SERVER_PATTERN_KEY, defaultCasServer);
137                }
138            }
139        }
140        log.debug("serviceUrl: " + url);
141        return url;
142    }
143
144    public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
145
146        // Check for an alternative authentication plugin in request cookies
147        NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse);
148        if (alternativeAuthPlugin != null) {
149            log.debug(String.format("Found alternative authentication plugin %s, using it to handle login prompt.",
150                    alternativeAuthPlugin));
151            return alternativeAuthPlugin.handleLoginPrompt(httpRequest, httpResponse, baseURL);
152        }
153
154        // Redirect to CAS Login screen
155        // passing our application URL as service name
156        String location = null;
157        try {
158            Map<String, String> urlParameters = new HashMap<String, String>();
159            urlParameters.put("service", getAppURL(httpRequest));
160            location = URIUtils.addParametersToURIQuery(getServiceURL(httpRequest, LOGIN_ACTION), urlParameters);
161            httpResponse.sendRedirect(location);
162        } catch (IOException e) {
163            log.error("Unable to redirect to CAS login screen to " + location, e);
164            return false;
165        }
166        return true;
167    }
168
169    protected String getAppURL(HttpServletRequest httpRequest) {
170        if (isValidStartupPage(httpRequest)) {
171            StringBuffer sb = new StringBuffer(VirtualHostHelper.getServerURL(httpRequest));
172            if (VirtualHostHelper.getServerURL(httpRequest).endsWith("/")) {
173                sb.deleteCharAt(sb.length() - 1);
174            }
175            sb.append(httpRequest.getRequestURI());
176            if (httpRequest.getQueryString() != null) {
177                sb.append("?");
178                sb.append(httpRequest.getQueryString());
179
180                // remove ticket parameter from URL to correctly validate the
181                // service
182                int indexTicketKey = sb.lastIndexOf(ticketKey + "=");
183                if (indexTicketKey != -1) {
184                    sb.delete(indexTicketKey - 1, sb.length());
185                }
186            }
187
188            return sb.toString();
189        }
190        if (appURL == null || appURL.equals("")) {
191            appURL = NUXEO_SERVER_PATTERN_KEY;
192        }
193        if (appURL.contains(NUXEO_SERVER_PATTERN_KEY)) {
194            String nxurl = BaseURL.getBaseURL(httpRequest);
195            return appURL.replace(NUXEO_SERVER_PATTERN_KEY, nxurl);
196        } else {
197            return appURL;
198        }
199    }
200
201    private boolean isValidStartupPage(HttpServletRequest httpRequest) {
202        if (httpRequest.getRequestURI() == null) {
203            return false;
204        }
205        PluggableAuthenticationService service = (PluggableAuthenticationService) Framework.getRuntime().getComponent(
206                PluggableAuthenticationService.NAME);
207        if (service == null) {
208            return false;
209        }
210        String startPage = httpRequest.getRequestURI().replace(VirtualHostHelper.getContextPath(httpRequest) + "/", "");
211        for (String prefix : service.getStartURLPatterns()) {
212            if (startPage.startsWith(prefix)) {
213                return true;
214            }
215        }
216        return false;
217    }
218
219    public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
220            HttpServletResponse httpResponse) {
221        String casTicket = httpRequest.getParameter(ticketKey);
222
223        // Retrieve the proxy parameter for knowing if the caller is à proxy
224        // CAS
225        String proxy = httpRequest.getParameter(proxyKey);
226
227        if (casTicket == null) {
228            log.debug("No ticket found");
229            return null;
230        }
231
232        String userName;
233
234        if (proxy == null) {
235            // no ticket found
236            userName = checkCasTicket(casTicket, httpRequest);
237        } else {
238            userName = checkProxyCasTicket(casTicket, httpRequest);
239        }
240
241        if (userName == null) {
242            return null;
243        }
244
245        UserIdentificationInfo uui = new UserIdentificationInfo(userName, casTicket);
246        uui.setToken(casTicket);
247
248        return uui;
249    }
250
251    public void initPlugin(Map<String, String> parameters) {
252        if (parameters.containsKey(CAS2Parameters.TICKET_NAME_KEY)) {
253            ticketKey = parameters.get(CAS2Parameters.TICKET_NAME_KEY);
254        }
255        if (parameters.containsKey(CAS2Parameters.PROXY_NAME_KEY)) {
256            proxyKey = parameters.get(CAS2Parameters.PROXY_NAME_KEY);
257        }
258        if (parameters.containsKey(CAS2Parameters.NUXEO_APP_URL_KEY)) {
259            appURL = parameters.get(CAS2Parameters.NUXEO_APP_URL_KEY);
260        }
261        if (parameters.containsKey(CAS2Parameters.SERVICE_LOGIN_URL_KEY)) {
262            serviceLoginURL = parameters.get(CAS2Parameters.SERVICE_LOGIN_URL_KEY);
263        }
264        if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATE_URL_KEY)) {
265            serviceValidateURL = parameters.get(CAS2Parameters.SERVICE_VALIDATE_URL_KEY);
266        }
267        if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATE_URL_KEY)) {
268            proxyValidateURL = parameters.get(CAS2Parameters.PROXY_VALIDATE_URL_KEY);
269        }
270        if (parameters.containsKey(CAS2Parameters.SERVICE_NAME_KEY)) {
271            serviceKey = parameters.get(CAS2Parameters.SERVICE_NAME_KEY);
272        }
273        if (parameters.containsKey(CAS2Parameters.LOGOUT_URL_KEY)) {
274            logoutURL = parameters.get(CAS2Parameters.LOGOUT_URL_KEY);
275        }
276        if (parameters.containsKey(CAS2Parameters.DEFAULT_CAS_SERVER_KEY)) {
277            defaultCasServer = parameters.get(CAS2Parameters.DEFAULT_CAS_SERVER_KEY);
278        }
279        if (parameters.containsKey(CAS2Parameters.SERVICE_VALIDATOR_CLASS)) {
280            ticketValidatorClassName = parameters.get(CAS2Parameters.SERVICE_VALIDATOR_CLASS);
281        }
282        if (parameters.containsKey(CAS2Parameters.PROXY_VALIDATOR_CLASS)) {
283            proxyValidatorClassName = parameters.get(CAS2Parameters.PROXY_VALIDATOR_CLASS);
284        }
285        if (parameters.containsKey(CAS2Parameters.PROMPT_LOGIN)) {
286            promptLogin = Boolean.parseBoolean(parameters.get(CAS2Parameters.PROMPT_LOGIN));
287        }
288        excludePromptURLs = new ArrayList<String>();
289        for (String key : parameters.keySet()) {
290            if (key.startsWith(EXCLUDE_PROMPT_KEY)) {
291                excludePromptURLs.add(parameters.get(key));
292            }
293        }
294        if (parameters.containsKey(CAS2Parameters.ERROR_PAGE)) {
295            errorPage = parameters.get(CAS2Parameters.ERROR_PAGE);
296        }
297    }
298
299    public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
300        String requestedURI = httpRequest.getRequestURI();
301        String context = httpRequest.getContextPath() + '/';
302        requestedURI = requestedURI.substring(context.length());
303        for (String prefixURL : excludePromptURLs) {
304            if (requestedURI.startsWith(prefixURL)) {
305                return false;
306            }
307        }
308        return promptLogin;
309    }
310
311    public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
312
313        // Check for an alternative authentication plugin in request cookies
314        NuxeoAuthenticationPlugin alternativeAuthPlugin = getAlternativeAuthPlugin(httpRequest, httpResponse);
315        if (alternativeAuthPlugin != null) {
316            if (alternativeAuthPlugin instanceof NuxeoAuthenticationPluginLogoutExtension) {
317                log.debug(String.format("Found alternative authentication plugin %s, using it to handle logout.",
318                        alternativeAuthPlugin));
319                return ((NuxeoAuthenticationPluginLogoutExtension) alternativeAuthPlugin).handleLogout(httpRequest,
320                        httpResponse);
321            } else {
322                log.debug(String.format(
323                        "Found alternative authentication plugin %s which cannot handle logout, letting authentication filter handle it.",
324                        alternativeAuthPlugin));
325                return false;
326            }
327        }
328
329        if (logoutURL == null || logoutURL.equals("")) {
330            log.debug("No CAS logout params, skipping CAS2Logout");
331            return false;
332        }
333        try {
334            httpResponse.sendRedirect(getServiceURL(httpRequest, LOGOUT_ACTION));
335        } catch (IOException e) {
336            log.error("Unable to redirect to CAS logout screen:", e);
337            return false;
338        }
339        return true;
340    }
341
342    protected String checkProxyCasTicket(String ticket, HttpServletRequest httpRequest) {
343        // Get the service passed by the portlet
344        String service = httpRequest.getParameter(serviceKey);
345        if (service == null) {
346            log.error("checkProxyCasTicket: no service name in the URL");
347            return null;
348        }
349
350        ProxyTicketValidator proxyValidator;
351        try {
352            proxyValidator = (ProxyTicketValidator) Framework.getRuntime().getContext().loadClass(
353                    proxyValidatorClassName).newInstance();
354        } catch (InstantiationException e) {
355            log.error(
356                    "checkProxyCasTicket during the ProxyTicketValidator initialization with InstantiationException:",
357                    e);
358            return null;
359        } catch (IllegalAccessException e) {
360            log.error(
361                    "checkProxyCasTicket during the ProxyTicketValidator initialization with IllegalAccessException:",
362                    e);
363            return null;
364        } catch (ClassNotFoundException e) {
365            log.error(
366                    "checkProxyCasTicket during the ProxyTicketValidator initialization with ClassNotFoundException:",
367                    e);
368            return null;
369        }
370
371        proxyValidator.setCasValidateUrl(getServiceURL(httpRequest, PROXY_VALIDATE_ACTION));
372        proxyValidator.setService(service);
373        proxyValidator.setServiceTicket(ticket);
374        try {
375            proxyValidator.validate();
376        } catch (IOException e) {
377            log.error("checkProxyCasTicket failed with IOException:", e);
378            return null;
379        } catch (SAXException e) {
380            log.error("checkProxyCasTicket failed with SAXException:", e);
381            return null;
382        } catch (ParserConfigurationException e) {
383            log.error("checkProxyCasTicket failed with ParserConfigurationException:", e);
384            return null;
385        }
386        log.debug("checkProxyCasTicket: validation executed without error");
387        String username = proxyValidator.getUser();
388        log.debug("checkProxyCasTicket: validation returned username = " + username);
389
390        return username;
391    }
392
393    // Cas2 Ticket management
394    protected String checkCasTicket(String ticket, HttpServletRequest httpRequest) {
395        ServiceTicketValidator ticketValidator;
396        try {
397            ticketValidator = (ServiceTicketValidator) Framework.getRuntime().getContext().loadClass(
398                    ticketValidatorClassName).newInstance();
399        } catch (InstantiationException e) {
400            log.error("checkCasTicket during the ServiceTicketValidator initialization with InstantiationException:", e);
401            return null;
402        } catch (IllegalAccessException e) {
403            log.error("checkCasTicket during the ServiceTicketValidator initialization with IllegalAccessException:", e);
404            return null;
405        } catch (ClassNotFoundException e) {
406            log.error("checkCasTicket during the ServiceTicketValidator initialization with ClassNotFoundException:", e);
407            return null;
408        }
409
410        ticketValidator.setCasValidateUrl(getServiceURL(httpRequest, VALIDATE_ACTION));
411        ticketValidator.setService(getAppURL(httpRequest));
412        ticketValidator.setServiceTicket(ticket);
413        try {
414            ticketValidator.validate();
415        } catch (IOException e) {
416            log.error("checkCasTicket failed with IOException:", e);
417            return null;
418        } catch (SAXException e) {
419            log.error("checkCasTicket failed with SAXException:", e);
420            return null;
421        } catch (ParserConfigurationException e) {
422            log.error("checkCasTicket failed with ParserConfigurationException:", e);
423            return null;
424        }
425        log.debug("checkCasTicket : validation executed without error");
426        String username = ticketValidator.getUser();
427        log.debug("checkCasTicket: validation returned username = " + username);
428        return username;
429    }
430
431    @Override
432    public boolean onError(HttpServletRequest request, HttpServletResponse response) {
433        try {
434            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
435            if (errorPage != null) {
436                response.sendRedirect(errorPage);
437            }
438        } catch (IOException e) {
439            log.error(e);
440            return false;
441        }
442        return true;
443    }
444
445    @Override
446    public boolean onSuccess(HttpServletRequest arg0, HttpServletResponse arg1) {
447        // TODO Auto-generated method stub
448        return false;
449    }
450
451    protected NuxeoAuthenticationPlugin getAlternativeAuthPlugin(HttpServletRequest httpRequest,
452            HttpServletResponse httpResponse) {
453
454        Cookie alternativeAuthPluginCookie = getCookie(httpRequest, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME);
455        if (alternativeAuthPluginCookie != null) {
456            String alternativeAuthPluginName = alternativeAuthPluginCookie.getValue();
457            PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime().getComponent(
458                    PluggableAuthenticationService.NAME);
459            NuxeoAuthenticationPlugin alternativeAuthPlugin = authService.getPlugin(alternativeAuthPluginName);
460            if (alternativeAuthPlugin == null) {
461                log.error(String.format("No alternative authentication plugin named %s, will remove cookie %s.",
462                        alternativeAuthPluginName, ALTERNATIVE_AUTH_PLUGIN_COOKIE_NAME));
463                removeCookie(httpRequest, httpResponse, alternativeAuthPluginCookie);
464            } else {
465                return alternativeAuthPlugin;
466            }
467        }
468        return null;
469    }
470
471    protected Cookie getCookie(HttpServletRequest httpRequest, String cookieName) {
472        Cookie cookies[] = httpRequest.getCookies();
473        if (cookies != null) {
474            for (int i = 0; i < cookies.length; i++) {
475                if (cookieName.equals(cookies[i].getName())) {
476                    return cookies[i];
477                }
478            }
479        }
480        return null;
481    }
482
483    protected void removeCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse, Cookie cookie) {
484        log.debug(String.format("Removing cookie %s.", cookie.getName()));
485        cookie.setMaxAge(0);
486        cookie.setValue("");
487        cookie.setPath(httpRequest.getContextPath());
488        httpResponse.addCookie(cookie);
489    }
490}