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> <filter> 100 * <filter-name>SpnegoHttpFilter</filter-name> 101 * <filter-class>net.sourceforge.spnego.SpnegoHttpFilter</filter-class> 102 * 103 * <init-param> 104 * <param-name>spnego.allow.basic</param-name> 105 * <param-value>true</param-value> 106 * </init-param> 107 * 108 * <init-param> 109 * <param-name>spnego.allow.localhost</param-name> 110 * <param-value>true</param-value> 111 * </init-param> 112 * 113 * <init-param> 114 * <param-name>spnego.allow.unsecure.basic</param-name> 115 * <param-value>true</param-value> 116 * </init-param> 117 * 118 * <init-param> 119 * <param-name>spnego.login.client.module</param-name> 120 * <param-value>spnego-client</param-value> 121 * </init-param> 122 * 123 * <init-param> 124 * <param-name>spnego.krb5.conf</param-name> 125 * <param-value>krb5.conf</param-value> 126 * </init-param> 127 * 128 * <init-param> 129 * <param-name>spnego.login.conf</param-name> 130 * <param-value>login.conf</param-value> 131 * </init-param> 132 * 133 * <init-param> 134 * <param-name>spnego.preauth.username</param-name> 135 * <param-value>Zeus</param-value> 136 * </init-param> 137 * 138 * <init-param> 139 * <param-name>spnego.preauth.password</param-name> 140 * <param-value>Zeus_Password</param-value> 141 * </init-param> 142 * 143 * <init-param> 144 * <param-name>spnego.login.server.module</param-name> 145 * <param-value>spnego-server</param-value> 146 * </init-param> 147 * 148 * <init-param> 149 * <param-name>spnego.prompt.ntlm</param-name> 150 * <param-value>true</param-value> 151 * </init-param> 152 * 153 * <init-param> 154 * <param-name>spnego.logger.level</param-name> 155 * <param-value>1</param-value> 156 * </init-param> 157 * </filter> 158 *</code></pre> 159 * </p> 160 * 161 * <p><b>Example usage on web page</b></p> 162 * 163 * <p><pre> <html> 164 * <head> 165 * <title>Hello SPNEGO Example</title> 166 * </head> 167 * <body> 168 * Hello <%= request.getRemoteUser() %> ! 169 * </body> 170 * </html> 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}