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    
019    package net.sourceforge.spnego;
020    
021    import java.io.FileNotFoundException;
022    import java.io.IOException;
023    import java.net.URISyntaxException;
024    import java.security.PrivilegedActionException;
025    import java.util.logging.Logger;
026    
027    import javax.security.auth.login.LoginException;
028    import javax.servlet.Filter;
029    import javax.servlet.FilterChain;
030    import javax.servlet.FilterConfig;
031    import javax.servlet.ServletException;
032    import javax.servlet.ServletRequest;
033    import javax.servlet.ServletResponse;
034    import javax.servlet.http.HttpServletRequest;
035    import javax.servlet.http.HttpServletResponse;
036    
037    import org.ietf.jgss.GSSException;
038    
039    /**
040     * Http Servlet Filter that provides <a
041     * href="http://en.wikipedia.org/wiki/SPNEGO" target="_blank">SPNEGO</a> authentication.
042     * It allows servlet containers like Tomcat and JBoss to transparently/silently
043     * authenticate HTTP clients like Microsoft Internet Explorer (MSIE).
044     * 
045     * <p>
046     * This feature in MSIE is sometimes referred to as single sign-on and/or 
047     * Integrated Windows Authentication. In general, there are at least two 
048     * authentication mechanisms that allow an HTTP server and an HTTP client 
049     * to achieve single sign-on: <b>NTLM</b> and <b>Kerberos/SPNEGO</b>.
050     * </p>
051     * 
052     * <p>
053     * <b>NTLM</b><br />
054     * MSIE has the ability to negotiate NTLM password hashes over an HTTP session 
055     * using Base 64 encoded NTLMSSP messages. This is a staple feature of Microsoft's 
056     * Internet Information Server (IIS). Open source libraries exists (ie. jCIFS) that 
057     * provide NTLM-based authentication capabilities to Servlet Containers. jCIFS uses 
058     * NTLM and Microsoft's Active Directory (AD) to authenticate MSIE clients.
059     * </p>
060     * 
061     * <p>
062     * <b>{@code SpnegoHttpFilter} does NOT support NTLM (tokens).</b>
063     * </p>
064     * 
065     * <p>
066     * <b>Kerberos/SPNEGO</b><br />
067     * Kerberos is an authentication protocol that is implemented in AD. The protocol 
068     * does not negotiate passwords between a client and a server but rather uses tokens 
069     * to securely prove/authenticate to one another over an un-secure network.
070     * </p>
071     * 
072     * <p>
073     * <b><code>SpnegoHttpFilter</code> does support Kerberos but through the 
074     * pseudo-mechanism <code>SPNEGO</code></b>.
075     * <ul>
076     * <li><a href="http://en.wikipedia.org/wiki/SPNEGO" target="_blank">Wikipedia: SPNEGO</a></li>
077     * <li><a href="http://www.ietf.org/rfc/rfc4178.txt" target="_blank">IETF RFC: 4178</a></li>
078     * </ul>
079     * </p>
080     * 
081     * <p>
082     * <b>Localhost Support</b><br />
083     * The Kerberos protocol requires that a service must have a Principal Name (SPN) 
084     * specified. However, there are some use-cases where it may not be practical to 
085     * specify an SPN (ie. Tomcat running on a developer's machine). The DNS 
086     * http://localhost is supported but must be configured in the servlet filter's 
087     * init params in the web.xml file. 
088     * </p>
089     * 
090     * <p><b>Modifying the web.xml file</b></p>
091     * 
092     * <p>Here's an example configuration:</p>
093     * 
094     * <p>
095     * <pre><code>  &lt;filter&gt;
096     *      &lt;filter-name&gt;SpnegoHttpFilter&lt;/filter-name&gt;
097     *      &lt;filter-class&gt;net.sourceforge.spnego.SpnegoHttpFilter&lt;/filter-class&gt;
098     *      
099     *      &lt;init-param&gt;
100     *          &lt;param-name&gt;spnego.allow.basic&lt;/param-name&gt;
101     *          &lt;param-value&gt;true&lt;/param-value&gt;
102     *      &lt;/init-param&gt;
103     *          
104     *      &lt;init-param&gt;
105     *          &lt;param-name&gt;spnego.allow.localhost&lt;/param-name&gt;
106     *          &lt;param-value&gt;true&lt;/param-value&gt;
107     *      &lt;/init-param&gt;
108     *          
109     *      &lt;init-param&gt;
110     *          &lt;param-name&gt;spnego.allow.unsecure.basic&lt;/param-name&gt;
111     *          &lt;param-value&gt;true&lt;/param-value&gt;
112     *      &lt;/init-param&gt;
113     *          
114     *      &lt;init-param&gt;
115     *          &lt;param-name&gt;spnego.login.client.module&lt;/param-name&gt;
116     *          &lt;param-value&gt;spnego-client&lt;/param-value&gt;
117     *      &lt;/init-param&gt;
118     *      
119     *      &lt;init-param&gt;
120     *          &lt;param-name&gt;spnego.krb5.conf&lt;/param-name&gt;
121     *          &lt;param-value&gt;krb5.conf&lt;/param-value&gt;
122     *      &lt;/init-param&gt;
123     *          
124     *      &lt;init-param&gt;
125     *          &lt;param-name&gt;spnego.login.conf&lt;/param-name&gt;
126     *          &lt;param-value&gt;login.conf&lt;/param-value&gt;
127     *      &lt;/init-param&gt;
128     *          
129     *      &lt;init-param&gt;
130     *          &lt;param-name&gt;spnego.preauth.username&lt;/param-name&gt;
131     *          &lt;param-value&gt;Zeus&lt;/param-value&gt;
132     *      &lt;/init-param&gt;
133     *          
134     *      &lt;init-param&gt;
135     *          &lt;param-name&gt;spnego.preauth.password&lt;/param-name&gt;
136     *          &lt;param-value&gt;Zeus_Password&lt;/param-value&gt;
137     *      &lt;/init-param&gt;
138     *          
139     *      &lt;init-param&gt;
140     *          &lt;param-name&gt;spnego.login.server.module&lt;/param-name&gt;
141     *          &lt;param-value&gt;spnego-server&lt;/param-value&gt;
142     *      &lt;/init-param&gt;
143     *          
144     *      &lt;init-param&gt;
145     *          &lt;param-name&gt;spnego.prompt.ntlm&lt;/param-name&gt;
146     *          &lt;param-value&gt;true&lt;/param-value&gt;
147     *      &lt;/init-param&gt;
148     *          
149     *      &lt;init-param&gt;
150     *          &lt;param-name&gt;spnego.logger.level&lt;/param-name&gt;
151     *          &lt;param-value&gt;1&lt;/param-value&gt;
152     *      &lt;/init-param&gt;
153     *  &lt;/filter&gt;
154     *</code></pre>
155     * </p>
156     * 
157     * <p><b>Example usage on web page</b></p>
158     * 
159     * <p><pre>  &lt;html&gt;
160     *  &lt;head&gt;
161     *      &lt;title&gt;Hello SPNEGO Example&lt;/title&gt;
162     *  &lt;/head&gt;
163     *  &lt;body&gt;
164     *  Hello &lt;%= request.getRemoteUser() %&gt; !
165     *  &lt;/body&gt;
166     *  &lt;/html&gt;
167     *  </pre>
168     * </p>
169     *
170     * <p>
171     * Take a look at the <a href="http://spnego.sourceforge.net/reference_docs.html" 
172     * target="_blank">reference docs</a> for other configuration parameters.
173     * </p>
174     * 
175     * <p>See more usage examples at 
176     * <a href="http://spnego.sourceforge.net" target="_blank">http://spnego.sourceforge.net</a>
177     * </p>
178     * 
179     * @author Darwin V. Felix
180     * 
181     */
182    public final class SpnegoHttpFilter implements Filter {
183    
184        private static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME);
185    
186        /** Object for performing Basic and SPNEGO authentication. */
187        private transient SpnegoAuthenticator authenticator = null;
188    
189        public void init(final FilterConfig filterConfig) throws ServletException {
190    
191            try {
192                // set some System properties
193                final SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(filterConfig);
194                
195                // pre-authenticate
196                this.authenticator = new SpnegoAuthenticator(config);
197            } catch (final LoginException le) {
198                throw new ServletException(le);
199            } catch (final GSSException gsse) {
200                throw new ServletException(gsse);
201            } catch (final PrivilegedActionException pae) {
202                throw new ServletException(pae);
203            } catch (final FileNotFoundException fnfe) {
204                throw new ServletException(fnfe);
205            } catch (final URISyntaxException uri) {
206                throw new ServletException(uri);
207            }
208        }
209    
210        @Override
211        public void destroy() {
212            if (null != this.authenticator) {
213                this.authenticator.dispose();
214                this.authenticator = null;
215            }
216        }
217    
218        @Override
219        public void doFilter(final ServletRequest request, final ServletResponse response
220            , final FilterChain chain) throws IOException, ServletException {
221    
222            final HttpServletRequest httpRequest = (HttpServletRequest) request;
223            final SpnegoHttpServletResponse spnegoResponse = new SpnegoHttpServletResponse(
224                    (HttpServletResponse) response);
225    
226            // client/caller principal
227            final SpnegoPrincipal principal;
228            try {
229                principal = this.authenticator.authenticate(httpRequest, spnegoResponse);
230            } catch (GSSException gsse) {
231                LOGGER.severe("HTTP Authorization Header="
232                    + httpRequest.getHeader(Constants.AUTHZ_HEADER));
233                throw new ServletException(gsse);
234            }
235    
236            // context/auth loop not yet complete
237            if (spnegoResponse.isStatusSet()) {
238                return;
239            }
240    
241            // assert
242            if (null == principal) {
243                LOGGER.severe("Principal was null.");
244                spnegoResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, true);
245                return;
246            }
247    
248            LOGGER.fine("principal=" + principal);
249    
250            chain.doFilter(new SpnegoHttpServletRequest(httpRequest, principal), response);
251        }
252        
253        /**
254         * Defines constants and parameter names that are used in the  
255         * web.xml file, and HTTP request headers, etc.
256         * 
257         * <p>
258         * This class is primarily used internally or by implementers of 
259         * custom http clients and by {@link SpnegoFilterConfig}.
260         * </p>
261         * 
262         */
263        public static final class Constants {
264    
265            private Constants() {
266                // default private
267            }
268            
269            /** 
270             * Servlet init param name in web.xml <b>spnego.allow.basic</b>.
271             * 
272             * <p>Set this value to <code>true</code> in web.xml if the filter 
273             * should allow Basic Authentication.</p>
274             * 
275             * <p>It is recommended that you only allow Basic Authentication 
276             * if you have clients that cannot perform Kerberos authentication. 
277             * Also, you should consider requiring SSL/TLS by setting 
278             * <code>spnego.allow.unsecure.basic</code> to <code>false</code>.</p>
279             */
280            public static final String ALLOW_BASIC = "spnego.allow.basic";
281    
282            /**
283             * Servlet init param name in web.xml <b>spnego.allow.delegation</b>.
284             * 
285             * <p>Set this value to <code>true</code> if server should support 
286             * credential delegation requests.</p>
287             * 
288             * <p>Take a look at the {@link DelegateServletRequest} for more 
289             * information about other pre-requisites.</p>
290             */
291            public static final String ALLOW_DELEGATION = "spnego.allow.delegation";
292            
293            /**
294             * Servlet init param name in web.xml <b>spnego.allow.localhost</b>.
295             * 
296             * <p>Flag to indicate if requests coming from http://localhost 
297             * or http://127.0.0.1 should not be authenticated using 
298             * Kerberos.</p>
299             * 
300             * <p>This feature helps to obviate the requirement of 
301             * creating an SPN for developer machines.</p>
302             * 
303             */
304            public static final String ALLOW_LOCALHOST = "spnego.allow.localhost";
305            
306            /** 
307             * Servlet init param name in web.xml <b>spnego.allow.unsecure.basic</b>.
308             * 
309             * <p>Set this value to <code>false</code> in web.xml if the filter 
310             * should reject connections that do not use SSL/TLS.</p>
311             */
312            public static final String ALLOW_UNSEC_BASIC = "spnego.allow.unsecure.basic";
313            
314            /** 
315             * HTTP Response Header <b>WWW-Authenticate</b>. 
316             * 
317             * <p>The filter will respond with this header with a value of "Basic" 
318             * and/or "Negotiate" (based on web.xml file).</p>
319             */
320            public static final String AUTHN_HEADER = "WWW-Authenticate";
321            
322            /** 
323             * HTTP Request Header <b>Authorization</b>. 
324             * 
325             * <p>Clients should send this header where the value is the 
326             * authentication token(s).</p>
327             */
328            public static final String AUTHZ_HEADER = "Authorization";
329            
330            /** 
331             * HTTP Response Header <b>Basic</b>. 
332             * 
333             * <p>The filter will set this as the value for the "WWW-Authenticate" 
334             * header if "Basic" auth is allowed (based on web.xml file).</p>
335             */
336            public static final String BASIC_HEADER = "Basic";
337            
338            /** 
339             * Servlet init param name in web.xml <b>spnego.login.client.module</b>. 
340             * 
341             * <p>The LoginModule name that exists in the login.conf file.</p>
342             */
343            public static final String CLIENT_MODULE = "spnego.login.client.module";
344    
345            /** 
346             * Servlet init param name in web.xml <b>spnego.krb5.conf</b>. 
347             * 
348             * <p>The location of the krb5.conf file. On Windows, this file will 
349             * sometimes be named krb5.ini and reside <code>%WINDOWS_ROOT%/krb5.ini</code> 
350             * here.</p>
351             * 
352             * <p>By default, Java looks for the file in these locations and order:
353             * <li>System Property (java.security.krb5.conf)</li>
354             * <li>%JAVA_HOME%/lib/security/krb5.conf</li>
355             * <li>%WINDOWS_ROOT%/krb5.ini</li>
356             * </p>
357             */
358            public static final String KRB5_CONF = "spnego.krb5.conf";
359            
360            /**
361             * Specify logging level.
362    
363             * <pre>
364             * 1 = FINEST
365             * 2 = FINER
366             * 3 = FINE
367             * 4 = CONFIG
368             * 5 = INFO
369             * 6 = WARNING
370             * 7 = SEVERE
371             * </pre>
372             * 
373             */
374            static final String LOGGER_LEVEL = "spnego.logger.level";
375            
376            /**
377             * Name of Spnego Logger.
378             * 
379             * <p>Example: <code>Logger.getLogger(Constants.LOGGER_NAME)</code></p>
380             */
381            static final String LOGGER_NAME = "SpnegoHttpFilter"; 
382            
383            /** 
384             * Servlet init param name in web.xml <b>spnego.login.conf</b>. 
385             * 
386             * <p>The location of the login.conf file.</p>
387             */
388            public static final String LOGIN_CONF = "spnego.login.conf";
389            
390            /** 
391             * HTTP Response Header <b>Negotiate</b>. 
392             * 
393             * <p>The filter will set this as the value for the "WWW-Authenticate" 
394             * header. Note that the filter may also add another header with 
395             * a value of "Basic" (if allowed by the web.xml file).</p>
396             */
397            public static final String NEGOTIATE_HEADER = "Negotiate";
398            
399            /**
400             * NTLM base64-encoded token start value.
401             */
402            static final String NTLM_PROLOG = "TlRMTVNT";
403            
404            /** 
405             * Servlet init param name in web.xml <b>spnego.preauth.password</b>. 
406             * 
407             * <p>Network Domain password. For Windows, this is sometimes known 
408             * as the Windows NT password.</p>
409             */
410            public static final String PREAUTH_PASSWORD = "spnego.preauth.password";
411            
412            /** 
413             * Servlet init param name in web.xml <b>spnego.preauth.username</b>. 
414             * 
415             * <p>Network Domain username. For Windows, this is sometimes known 
416             * as the Windows NT username.</p>
417             */
418            public static final String PREAUTH_USERNAME = "spnego.preauth.username";
419            
420            /**
421             * If server receives an NTLM token, the filter will return with a 401 
422             * and with Basic as the only option (no Negotiate) <b>spnego.prompt.ntlm</b>. 
423             */
424            public static final String PROMPT_NTLM = "spnego.prompt.ntlm";
425            
426            /** 
427             * Servlet init param name in web.xml <b>spnego.login.server.module</b>. 
428             * 
429             * <p>The LoginModule name that exists in the login.conf file.</p>
430             */
431            public static final String SERVER_MODULE = "spnego.login.server.module";
432        }
433    }