001/** 
002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net>
003 * 
004 * This library is free software; you can redistribute it and/or
005 * modify it under the terms of the GNU Lesser General Public
006 * License as published by the Free Software Foundation; either
007 * version 2.1 of the License, or (at your option) any later version.
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 * You should have received a copy of the GNU Lesser General Public
015 * License along with this library; if not, write to the Free Software
016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017 */
018
019package net.sourceforge.spnego;
020
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.net.URISyntaxException;
024import java.security.PrivilegedActionException;
025import java.util.ArrayList;
026import java.util.Enumeration;
027import java.util.List;
028import java.util.Properties;
029import java.util.logging.Logger;
030
031import javax.security.auth.login.LoginException;
032import javax.servlet.Filter;
033import javax.servlet.FilterChain;
034import javax.servlet.FilterConfig;
035import javax.servlet.ServletException;
036import javax.servlet.ServletRequest;
037import javax.servlet.ServletResponse;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040
041import org.ietf.jgss.GSSException;
042
043/**
044 * Http Servlet Filter that provides <a
045 * href="http://en.wikipedia.org/wiki/SPNEGO" target="_blank">SPNEGO</a> authentication.
046 * It allows servlet containers like Tomcat and JBoss to transparently/silently
047 * authenticate HTTP clients like Microsoft Internet Explorer (MSIE).
048 * 
049 * <p>
050 * This feature in MSIE is sometimes referred to as single sign-on and/or 
051 * Integrated Windows Authentication. In general, there are at least two 
052 * authentication mechanisms that allow an HTTP server and an HTTP client 
053 * to achieve single sign-on: <b>NTLM</b> and <b>Kerberos/SPNEGO</b>.
054 * </p>
055 * 
056 * <p>
057 * <b>NTLM</b><br />
058 * MSIE has the ability to negotiate NTLM password hashes over an HTTP session 
059 * using Base 64 encoded NTLMSSP messages. This is a staple feature of Microsoft's 
060 * Internet Information Server (IIS). Open source libraries exists (ie. jCIFS) that 
061 * provide NTLM-based authentication capabilities to Servlet Containers. jCIFS uses 
062 * NTLM and Microsoft's Active Directory (AD) to authenticate MSIE clients.
063 * </p>
064 * 
065 * <p>
066 * <b>{@code SpnegoHttpFilter} does NOT support NTLM (tokens).</b>
067 * </p>
068 * 
069 * <p>
070 * <b>Kerberos/SPNEGO</b><br />
071 * Kerberos is an authentication protocol that is implemented in AD. The protocol 
072 * does not negotiate passwords between a client and a server but rather uses tokens 
073 * to securely prove/authenticate to one another over an un-secure network.
074 * </p>
075 * 
076 * <p>
077 * <b><code>SpnegoHttpFilter</code> does support Kerberos but through the 
078 * pseudo-mechanism <code>SPNEGO</code></b>.
079 * <ul>
080 * <li><a href="http://en.wikipedia.org/wiki/SPNEGO" target="_blank">Wikipedia: SPNEGO</a></li>
081 * <li><a href="http://www.ietf.org/rfc/rfc4178.txt" target="_blank">IETF RFC: 4178</a></li>
082 * </ul>
083 * </p>
084 * 
085 * <p>
086 * <b>Localhost Support</b><br />
087 * The Kerberos protocol requires that a service must have a Principal Name (SPN) 
088 * specified. However, there are some use-cases where it may not be practical to 
089 * specify an SPN (ie. Tomcat running on a developer's machine). The DNS 
090 * http://localhost is supported but must be configured in the servlet filter's 
091 * init params in the web.xml file. 
092 * </p>
093 * 
094 * <p><b>Modifying the web.xml file</b></p>
095 * 
096 * <p>Here's an example configuration:</p>
097 * 
098 * <p>
099 * <pre><code>  &lt;filter&gt;
100 *      &lt;filter-name&gt;SpnegoHttpFilter&lt;/filter-name&gt;
101 *      &lt;filter-class&gt;net.sourceforge.spnego.SpnegoHttpFilter&lt;/filter-class&gt;
102 *      
103 *      &lt;init-param&gt;
104 *          &lt;param-name&gt;spnego.allow.basic&lt;/param-name&gt;
105 *          &lt;param-value&gt;true&lt;/param-value&gt;
106 *      &lt;/init-param&gt;
107 *          
108 *      &lt;init-param&gt;
109 *          &lt;param-name&gt;spnego.allow.localhost&lt;/param-name&gt;
110 *          &lt;param-value&gt;true&lt;/param-value&gt;
111 *      &lt;/init-param&gt;
112 *          
113 *      &lt;init-param&gt;
114 *          &lt;param-name&gt;spnego.allow.unsecure.basic&lt;/param-name&gt;
115 *          &lt;param-value&gt;true&lt;/param-value&gt;
116 *      &lt;/init-param&gt;
117 *          
118 *      &lt;init-param&gt;
119 *          &lt;param-name&gt;spnego.login.client.module&lt;/param-name&gt;
120 *          &lt;param-value&gt;spnego-client&lt;/param-value&gt;
121 *      &lt;/init-param&gt;
122 *      
123 *      &lt;init-param&gt;
124 *          &lt;param-name&gt;spnego.krb5.conf&lt;/param-name&gt;
125 *          &lt;param-value&gt;krb5.conf&lt;/param-value&gt;
126 *      &lt;/init-param&gt;
127 *          
128 *      &lt;init-param&gt;
129 *          &lt;param-name&gt;spnego.login.conf&lt;/param-name&gt;
130 *          &lt;param-value&gt;login.conf&lt;/param-value&gt;
131 *      &lt;/init-param&gt;
132 *          
133 *      &lt;init-param&gt;
134 *          &lt;param-name&gt;spnego.preauth.username&lt;/param-name&gt;
135 *          &lt;param-value&gt;Zeus&lt;/param-value&gt;
136 *      &lt;/init-param&gt;
137 *          
138 *      &lt;init-param&gt;
139 *          &lt;param-name&gt;spnego.preauth.password&lt;/param-name&gt;
140 *          &lt;param-value&gt;Zeus_Password&lt;/param-value&gt;
141 *      &lt;/init-param&gt;
142 *          
143 *      &lt;init-param&gt;
144 *          &lt;param-name&gt;spnego.login.server.module&lt;/param-name&gt;
145 *          &lt;param-value&gt;spnego-server&lt;/param-value&gt;
146 *      &lt;/init-param&gt;
147 *          
148 *      &lt;init-param&gt;
149 *          &lt;param-name&gt;spnego.prompt.ntlm&lt;/param-name&gt;
150 *          &lt;param-value&gt;true&lt;/param-value&gt;
151 *      &lt;/init-param&gt;
152 *          
153 *      &lt;init-param&gt;
154 *          &lt;param-name&gt;spnego.logger.level&lt;/param-name&gt;
155 *          &lt;param-value&gt;1&lt;/param-value&gt;
156 *      &lt;/init-param&gt;
157 *  &lt;/filter&gt;
158 *</code></pre>
159 * </p>
160 * 
161 * <p><b>Example usage on web page</b></p>
162 * 
163 * <p><pre>  &lt;html&gt;
164 *  &lt;head&gt;
165 *      &lt;title&gt;Hello SPNEGO Example&lt;/title&gt;
166 *  &lt;/head&gt;
167 *  &lt;body&gt;
168 *  Hello &lt;%= request.getRemoteUser() %&gt; !
169 *  &lt;/body&gt;
170 *  &lt;/html&gt;
171 *  </pre>
172 * </p>
173 *
174 * <p>
175 * Take a look at the <a href="http://spnego.sourceforge.net/reference_docs.html" 
176 * target="_blank">reference docs</a> for other configuration parameters.
177 * </p>
178 * 
179 * <p>See more usage examples at 
180 * <a href="http://spnego.sourceforge.net" target="_blank">http://spnego.sourceforge.net</a>
181 * </p>
182 * 
183 * @author Darwin V. Felix
184 * 
185 */
186public final class SpnegoHttpFilter implements Filter {
187
188    private static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME);
189
190    /** Object for performing Basic and SPNEGO authentication. */
191    private transient SpnegoAuthenticator authenticator;
192    
193    /** Object for performing User Authorization. */
194    private transient UserAccessControl accessControl;
195    
196    /** AuthZ required for every page. */
197    private transient String sitewide;
198    
199    /** Landing page if user is denied authZ access. */
200    private transient String page403;
201    
202    /** directories which should not be authenticated irrespective of filter-mapping. */
203    private final transient List<String> excludeDirs = new ArrayList<String>();
204    
205    @Override
206    public void init(final FilterConfig filterConfig) throws ServletException {
207
208        try {
209            // set some System properties
210            final SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(filterConfig);
211            this.excludeDirs.addAll(config.getExcludeDirs());
212            
213            LOGGER.info("excludeDirs=" + this.excludeDirs);
214            
215            // pre-authenticate
216            this.authenticator = new SpnegoAuthenticator(config);
217            
218            // authorization
219            final Properties props = SpnegoHttpFilter.toProperties(filterConfig);
220            if (!props.getProperty("spnego.authz.class", "").isEmpty()) {
221                props.put("spnego.server.realm", this.authenticator.getServerRealm());
222                this.page403 = props.getProperty("spnego.authz.403", "").trim();
223                this.sitewide = props.getProperty("spnego.authz.sitewide", "").trim();
224                this.sitewide = (this.sitewide.isEmpty()) ? null : this.sitewide;
225                this.accessControl = (UserAccessControl) Class.forName(
226                        props.getProperty("spnego.authz.class")).newInstance();
227                this.accessControl.init(props);                
228            }
229            
230        } catch (final LoginException lex) {
231            throw new ServletException(lex);
232        } catch (final GSSException gsse) {
233            throw new ServletException(gsse);
234        } catch (final PrivilegedActionException pae) {
235            throw new ServletException(pae);
236        } catch (final FileNotFoundException fnfe) {
237            throw new ServletException(fnfe);
238        } catch (final URISyntaxException uri) {
239            throw new ServletException(uri);
240        } catch (InstantiationException iex) {
241            throw new ServletException(iex);
242        } catch (IllegalAccessException iae) {
243            throw new ServletException(iae);
244        } catch (ClassNotFoundException cnfe) {
245            throw new ServletException(cnfe);
246        }
247    }
248
249    @Override
250    public void destroy() {
251        this.page403 = null;
252        this.sitewide = null;
253        if (null != this.excludeDirs) {
254            this.excludeDirs.clear();
255        }
256        if (null != this.accessControl) {
257            this.accessControl.destroy();
258            this.accessControl = null;
259        }
260        if (null != this.authenticator) {
261            this.authenticator.dispose();
262            this.authenticator = null;
263        }
264    }
265
266    @Override
267    public void doFilter(final ServletRequest request, final ServletResponse response
268        , final FilterChain chain) throws IOException, ServletException {
269
270        final HttpServletRequest httpRequest = (HttpServletRequest) request;
271        final SpnegoHttpServletResponse spnegoResponse = new SpnegoHttpServletResponse(
272                (HttpServletResponse) response);
273        
274        // skip authentication if resource is in the list of directories to exclude
275        if (exclude(httpRequest.getContextPath(), httpRequest.getServletPath())) {
276            chain.doFilter(request, response);
277            return;
278        }
279        
280        // client/caller principal
281        final SpnegoPrincipal principal;
282        try {
283            principal = this.authenticator.authenticate(httpRequest, spnegoResponse);
284        } catch (GSSException gsse) {
285            LOGGER.severe("HTTP Authorization Header="
286                + httpRequest.getHeader(Constants.AUTHZ_HEADER));
287            throw new ServletException(gsse);
288        }
289
290        // context/auth loop not yet complete
291        if (spnegoResponse.isStatusSet()) {
292            return;
293        }
294
295        // assert
296        if (null == principal) {
297            LOGGER.severe("Principal was null.");
298            spnegoResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, true);
299            return;
300        }
301
302        LOGGER.fine("principal=" + principal);
303        
304        final SpnegoHttpServletRequest spnegoRequest = 
305                new SpnegoHttpServletRequest(httpRequest, principal, this.accessControl);
306                
307        // site wide authZ check (if enabled)
308        if (!isAuthorized((HttpServletRequest) spnegoRequest)) {
309            LOGGER.info("Principal Not AuthoriZed: " + principal);
310            if (this.page403.isEmpty()) {
311                spnegoResponse.setStatus(HttpServletResponse.SC_FORBIDDEN, true);  
312            } else {
313                request.getRequestDispatcher(this.page403).forward(spnegoRequest, response);
314            }
315            return;            
316        }
317
318        chain.doFilter(spnegoRequest, response);
319    }
320    
321    private boolean isAuthorized(final HttpServletRequest request) {
322        if (null != this.sitewide && null != this.accessControl
323                && !this.accessControl.hasAccess(request.getRemoteUser(), this.sitewide)) {
324            return false;
325        }
326
327        return true;
328    }
329    
330    private boolean exclude(final String contextPath, final String servletPath) {
331        // each item in excludeDirs ends with a slash
332        final String path = contextPath + servletPath + (servletPath.endsWith("/") ? "" : "/");
333        
334        for (String dir : this.excludeDirs) {
335            if (path.startsWith(dir)) {
336                return true;
337            }
338        }
339        
340        return false;
341    }
342    
343    private static Properties toProperties(final FilterConfig filterConfig) {
344        final Properties props = new Properties();
345        @SuppressWarnings("unchecked")
346        final Enumeration<String> it = filterConfig.getInitParameterNames();
347        
348        while (it.hasMoreElements()) {
349            final String key = it.nextElement();
350            props.put(key, filterConfig.getInitParameter(key));
351        }
352        
353        return props;
354    }
355    
356    /**
357     * Defines constants and parameter names that are used in the  
358     * web.xml file, and HTTP request headers, etc.
359     * 
360     * <p>
361     * This class is primarily used internally or by implementers of 
362     * custom http clients and by {@link SpnegoFilterConfig}.
363     * </p>
364     * 
365     */
366    public static final class Constants {
367
368        private Constants() {
369            // default private
370        }
371        
372        /** 
373         * Servlet init param name in web.xml <b>spnego.allow.basic</b>.
374         * 
375         * <p>Set this value to <code>true</code> in web.xml if the filter 
376         * should allow Basic Authentication.</p>
377         * 
378         * <p>It is recommended that you only allow Basic Authentication 
379         * if you have clients that cannot perform Kerberos authentication. 
380         * Also, you should consider requiring SSL/TLS by setting 
381         * <code>spnego.allow.unsecure.basic</code> to <code>false</code>.</p>
382         */
383        public static final String ALLOW_BASIC = "spnego.allow.basic";
384
385        /**
386         * Servlet init param name in web.xml <b>spnego.allow.delegation</b>.
387         * 
388         * <p>Set this value to <code>true</code> if server should support 
389         * credential delegation requests.</p>
390         * 
391         * <p>Take a look at the {@link DelegateServletRequest} for more 
392         * information about other pre-requisites.</p>
393         */
394        public static final String ALLOW_DELEGATION = "spnego.allow.delegation";
395        
396        /**
397         * Servlet init param name in web.xml <b>spnego.allow.localhost</b>.
398         * 
399         * <p>Flag to indicate if requests coming from http://localhost 
400         * or http://127.0.0.1 should not be authenticated using 
401         * Kerberos.</p>
402         * 
403         * <p>This feature helps to obviate the requirement of 
404         * creating an SPN for developer machines.</p>
405         * 
406         */
407        public static final String ALLOW_LOCALHOST = "spnego.allow.localhost";
408        
409        /** 
410         * Servlet init param name in web.xml <b>spnego.allow.unsecure.basic</b>.
411         * 
412         * <p>Set this value to <code>false</code> in web.xml if the filter 
413         * should reject connections that do not use SSL/TLS.</p>
414         */
415        public static final String ALLOW_UNSEC_BASIC = "spnego.allow.unsecure.basic";
416        
417        /** 
418         * HTTP Response Header <b>WWW-Authenticate</b>. 
419         * 
420         * <p>The filter will respond with this header with a value of "Basic" 
421         * and/or "Negotiate" (based on web.xml file).</p>
422         */
423        public static final String AUTHN_HEADER = "WWW-Authenticate";
424        
425        /** 
426         * HTTP Request Header <b>Authorization</b>. 
427         * 
428         * <p>Clients should send this header where the value is the 
429         * authentication token(s).</p>
430         */
431        public static final String AUTHZ_HEADER = "Authorization";
432        
433        /** 
434         * HTTP Response Header <b>Basic</b>. 
435         * 
436         * <p>The filter will set this as the value for the "WWW-Authenticate" 
437         * header if "Basic" auth is allowed (based on web.xml file).</p>
438         */
439        public static final String BASIC_HEADER = "Basic";
440        
441        /** 
442         * Servlet init param name in web.xml <b>spnego.login.client.module</b>. 
443         * 
444         * <p>The LoginModule name that exists in the login.conf file.</p>
445         */
446        public static final String CLIENT_MODULE = "spnego.login.client.module";
447
448        /** 
449         * HTTP Request Header <b>Content-Type</b>. 
450         * 
451         */
452        public static final String CONTENT_TYPE = "Content-Type";
453        
454        /** 
455         * Servlet init param name in web.xml <b>spnego.exclude.dirs</b>.
456         * 
457         * <p>
458         * A List of URL paths, starting at the context root, 
459         * that should NOT undergo authentication (authN). 
460         * </p>
461         */
462        public static final String EXCLUDE_DIRS = "spnego.exclude.dirs";
463        
464        /** 
465         * Servlet init param name in web.xml <b>spnego.krb5.conf</b>. 
466         * 
467         * <p>The location of the krb5.conf file. On Windows, this file will 
468         * sometimes be named krb5.ini and reside <code>%WINDOWS_ROOT%/krb5.ini</code> 
469         * here.</p>
470         * 
471         * <p>By default, Java looks for the file in these locations and order:
472         * <li>System Property (java.security.krb5.conf)</li>
473         * <li>%JAVA_HOME%/lib/security/krb5.conf</li>
474         * <li>%WINDOWS_ROOT%/krb5.ini</li>
475         * </p>
476         */
477        public static final String KRB5_CONF = "spnego.krb5.conf";
478        
479        /**
480         * Specify logging level.
481
482         * <pre>
483         * 1 = FINEST
484         * 2 = FINER
485         * 3 = FINE
486         * 4 = CONFIG
487         * 5 = INFO
488         * 6 = WARNING
489         * 7 = SEVERE
490         * </pre>
491         * 
492         */
493        static final String LOGGER_LEVEL = "spnego.logger.level";
494        
495        /**
496         * Name of Spnego Logger.
497         * 
498         * <p>Example: <code>Logger.getLogger(Constants.LOGGER_NAME)</code></p>
499         */
500        static final String LOGGER_NAME = "SpnegoHttpFilter"; 
501        
502        /** 
503         * Servlet init param name in web.xml <b>spnego.login.conf</b>. 
504         * 
505         * <p>The location of the login.conf file.</p>
506         */
507        public static final String LOGIN_CONF = "spnego.login.conf";
508        
509        /** 
510         * HTTP Response Header <b>Negotiate</b>. 
511         * 
512         * <p>The filter will set this as the value for the "WWW-Authenticate" 
513         * header. Note that the filter may also add another header with 
514         * a value of "Basic" (if allowed by the web.xml file).</p>
515         */
516        public static final String NEGOTIATE_HEADER = "Negotiate";
517        
518        /**
519         * NTLM base64-encoded token start value.
520         */
521        static final String NTLM_PROLOG = "TlRMTVNT";
522        
523        /** 
524         * Servlet init param name in web.xml <b>spnego.preauth.password</b>. 
525         * 
526         * <p>Network Domain password. For Windows, this is sometimes known 
527         * as the Windows NT password.</p>
528         */
529        public static final String PREAUTH_PASSWORD = "spnego.preauth.password";
530        
531        /** 
532         * Servlet init param name in web.xml <b>spnego.preauth.username</b>. 
533         * 
534         * <p>Network Domain username. For Windows, this is sometimes known 
535         * as the Windows NT username.</p>
536         */
537        public static final String PREAUTH_USERNAME = "spnego.preauth.username";
538        
539        /**
540         * If server receives an NTLM token, the filter will return with a 401 
541         * and with Basic as the only option (no Negotiate) <b>spnego.prompt.ntlm</b>. 
542         */
543        public static final String PROMPT_NTLM = "spnego.prompt.ntlm";
544        
545        /** 
546         * Servlet init param name in web.xml <b>spnego.login.server.module</b>. 
547         * 
548         * <p>The LoginModule name that exists in the login.conf file.</p>
549         */
550        public static final String SERVER_MODULE = "spnego.login.server.module";
551        
552        /** 
553         * HTTP Request Header <b>SOAPAction</b>. 
554         * 
555         */
556        public static final String SOAP_ACTION = "SOAPAction";
557    }
558}