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