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.Collections; 026 import java.util.Enumeration; 027 import java.util.Map; 028 import java.util.concurrent.locks.Lock; 029 import java.util.concurrent.locks.ReentrantLock; 030 import java.util.logging.Level; 031 import java.util.logging.Logger; 032 033 import javax.security.auth.callback.CallbackHandler; 034 import javax.security.auth.kerberos.KerberosPrincipal; 035 import javax.security.auth.login.LoginContext; 036 import javax.security.auth.login.LoginException; 037 import javax.servlet.FilterConfig; 038 import javax.servlet.ServletContext; 039 import javax.servlet.http.HttpServletRequest; 040 import javax.servlet.http.HttpServletResponse; 041 042 import net.sourceforge.spnego.SpnegoHttpFilter.Constants; 043 044 import org.ietf.jgss.GSSContext; 045 import org.ietf.jgss.GSSCredential; 046 import org.ietf.jgss.GSSException; 047 import org.ietf.jgss.GSSManager; 048 049 /** 050 * Handles <a href="http://en.wikipedia.org/wiki/SPNEGO">SPNEGO</a> or <a 051 * href="http://en.wikipedia.org/wiki/Basic_access_authentication">Basic</a> 052 * authentication. 053 * 054 * <p> 055 * <strike>Package scope is deliberate; this Class MUST NOT be used/referenced directly 056 * outside of this package.</strike> <b>Be cautious about who you give a reference to.</b> 057 * </p> 058 * 059 * <p> 060 * Basic Authentication must be enabled through the filter configuration. See 061 * an example web.xml configuration in the <a href="http://spnego.sourceforge.net/spnego_tomcat.html" 062 * target="_blank">installing on tomcat</a> documentation or the 063 * {@link SpnegoHttpFilter} javadoc. 064 * </p> 065 * 066 * <p> 067 * Localhost is supported but must be enabled through the filter configuration. Allowing 068 * requests to come from the DNS http://localhost will obviate the requirement that a 069 * service must have an SPN. <b>Note that Kerberos authentication (if localhost) does 070 * not occur but instead simply returns the <code>System.getProperty("user.name")</code> 071 * or the Server's pre-authentication username.</b> 072 * </p> 073 * 074 * <p> 075 * NTLM tokens are NOT supported. However it is still possible to avoid an error 076 * being returned by downgrading the authentication from Negotiate NTLM to Basic Auth. 077 * </p> 078 * 079 * <p> 080 * See the <a href="http://spnego.sourceforge.net/reference_docs.html" 081 * target="_blank">reference docs</a> on how to configure the web.xml to prompt 082 * when if a request is being made using NTLM. 083 * </p> 084 * 085 * <p> 086 * Finally, to see a working example and instructions on how to use a keytab, take 087 * a look at the <a href="http://spnego.sourceforge.net/server_keytab.html" 088 * target="_blank">creating a server keytab</a> example. 089 * </p> 090 * 091 * @author Darwin V. Felix 092 * 093 */ 094 public final class SpnegoAuthenticator { 095 096 private static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME); 097 098 /** GSSContext is not thread-safe. */ 099 private static final Lock LOCK = new ReentrantLock(); 100 101 /** Default GSSManager. */ 102 private static final GSSManager MANAGER = GSSManager.getInstance(); 103 104 /** Flag to indicate if BASIC Auth is allowed. */ 105 private final transient boolean allowBasic; 106 107 /** Flag to indicate if credential delegation is allowed. */ 108 private final transient boolean allowDelegation; 109 110 /** Flag to skip auth if localhost. */ 111 private final transient boolean allowLocalhost; 112 113 /** Flag to indicate if non-SSL BASIC Auth allowed. */ 114 private final transient boolean allowUnsecure; 115 116 /** Flag to indicate if NTLM is accepted. */ 117 private final transient boolean promptIfNtlm; 118 119 /** Login Context module name for client auth. */ 120 private final transient String clientModuleName; 121 122 /** Login Context server uses for pre-authentication. */ 123 private final transient LoginContext loginContext; 124 125 /** Credentials server uses for authenticating requests. */ 126 private final transient GSSCredential serverCredentials; 127 128 /** Server Principal used for pre-authentication. */ 129 private final transient KerberosPrincipal serverPrincipal; 130 131 /** 132 * Create an authenticator for SPNEGO and/or BASIC authentication. 133 * 134 * @param config servlet filter initialization parameters 135 * @throws LoginException 136 * @throws GSSException 137 * @throws PrivilegedActionException 138 */ 139 public SpnegoAuthenticator(final SpnegoFilterConfig config) 140 throws LoginException, GSSException, PrivilegedActionException { 141 142 LOGGER.fine("config=" + config); 143 144 this.allowBasic = config.isBasicAllowed(); 145 this.allowUnsecure = config.isUnsecureAllowed(); 146 this.clientModuleName = config.getClientLoginModule(); 147 this.allowLocalhost = config.isLocalhostAllowed(); 148 this.promptIfNtlm = config.downgradeNtlm(); 149 this.allowDelegation = config.isDelegationAllowed(); 150 151 if (config.useKeyTab()) { 152 this.loginContext = new LoginContext(config.getServerLoginModule()); 153 } else { 154 final CallbackHandler handler = SpnegoProvider.getUsernamePasswordHandler( 155 config.getPreauthUsername() 156 , config.getPreauthPassword()); 157 158 this.loginContext = new LoginContext(config.getServerLoginModule(), handler); 159 } 160 161 this.loginContext.login(); 162 163 this.serverCredentials = SpnegoProvider.getServerCredential( 164 this.loginContext.getSubject()); 165 166 this.serverPrincipal = new KerberosPrincipal( 167 this.serverCredentials.getName().toString()); 168 } 169 170 /** 171 * Create an authenticator for SPNEGO and/or BASIC authentication. For third-party 172 * code/frameworks that want to authenticate via their own filter/valve/code/etc. 173 * 174 * <p> 175 * The ExampleSpnegoAuthenticatorValve.java demonstrates a working example of 176 * how to use this constructor. 177 * </p> 178 * 179 * <p> 180 * Example of some Map keys and values: <br /> 181 * <pre> 182 * 183 * Map map = new HashMap(); 184 * map.put("spnego.krb5.conf", "krb5.conf"); 185 * map.put("spnego.allow.basic", "true"); 186 * map.put("spnego.preauth.username", "dfelix"); 187 * map.put("spnego.preauth.password", "myp@s5"); 188 * ... 189 * 190 * SpnegoAuthenticator authenticator = new SpnegoAuthenticator(map); 191 * ... 192 * </pre> 193 * </p> 194 * 195 * @param config 196 * @throws LoginException 197 * @throws GSSException 198 * @throws PrivilegedActionException 199 * @throws FileNotFoundException 200 * @throws URISyntaxException 201 */ 202 public SpnegoAuthenticator(final Map<String, String> config) 203 throws LoginException, GSSException, PrivilegedActionException 204 , FileNotFoundException, URISyntaxException { 205 206 this(SpnegoFilterConfig.getInstance(new FilterConfig() { 207 208 private final Map<String, String> map = Collections.unmodifiableMap(config); 209 210 @Override 211 public String getFilterName() { 212 throw new UnsupportedOperationException(); 213 } 214 215 @Override 216 public String getInitParameter(final String param) { 217 if (null == map.get(param)) { 218 throw new NullPointerException("Config missing param value for: " + param); 219 } 220 return map.get(param); 221 } 222 223 @SuppressWarnings("rawtypes") 224 @Override 225 public Enumeration getInitParameterNames() { 226 throw new UnsupportedOperationException(); 227 } 228 229 @Override 230 public ServletContext getServletContext() { 231 throw new UnsupportedOperationException(); 232 } 233 })); 234 } 235 236 /** 237 * Returns the KerberosPrincipal of the user/client making the HTTP request. 238 * 239 * <p> 240 * Null may be returned if client did not provide auth info. 241 * </p> 242 * 243 * <p> 244 * Method will throw UnsupportedOperationException if client authz 245 * request is NOT "Negotiate" or "Basic". 246 * </p> 247 * @param req servlet request 248 * @param resp servlet response 249 * 250 * @return null if auth not complete else SpnegoPrincipal of client 251 * @throws GSSException 252 * @throws IOException 253 */ 254 public SpnegoPrincipal authenticate(final HttpServletRequest req 255 , final SpnegoHttpServletResponse resp) throws GSSException 256 , IOException { 257 258 // determine if we allow basic 259 final boolean basicSupported = 260 this.allowBasic && (this.allowUnsecure || req.isSecure()); 261 262 // domain/realm of server 263 final String serverRealm = this.serverPrincipal.getRealm(); 264 265 // Skip auth if localhost 266 if (this.allowLocalhost && this.isLocalhost(req)) { 267 return doLocalhost(); 268 } 269 270 final SpnegoPrincipal principal; 271 final SpnegoAuthScheme scheme = SpnegoProvider.negotiate( 272 req, resp, basicSupported, this.promptIfNtlm, serverRealm); 273 274 // NOTE: this may also occur if we do not allow Basic Auth and 275 // the client only supports Basic Auth 276 if (null == scheme) { 277 LOGGER.finer("scheme null."); 278 return null; 279 } 280 281 // NEGOTIATE scheme 282 if (scheme.isNegotiateScheme()) { 283 principal = doSpnegoAuth(scheme, resp); 284 285 // BASIC scheme 286 } else if (scheme.isBasicScheme()) { 287 // check if we allow Basic Auth 288 if (basicSupported) { 289 principal = doBasicAuth(scheme, resp); 290 } else { 291 LOGGER.severe("allowBasic=" + this.allowBasic 292 + "; allowUnsecure=" + this.allowUnsecure 293 + "; req.isSecure()=" + req.isSecure()); 294 throw new UnsupportedOperationException("Basic Auth not allowed" 295 + " or SSL required."); 296 } 297 298 // Unsupported scheme 299 } else { 300 throw new UnsupportedOperationException("scheme=" + scheme); 301 } 302 303 return principal; 304 } 305 306 /** 307 * Logout. Since server uses LoginContext to login/pre-authenticate, we must 308 * also logout when we are done using this object. 309 * 310 * <p> 311 * Generally, instantiators of this class should be the only to call 312 * dispose() as it indicates that this class will no longer be used. 313 * </p> 314 */ 315 public void dispose() { 316 if (null != this.serverCredentials) { 317 try { 318 this.serverCredentials.dispose(); 319 } catch (GSSException e) { 320 LOGGER.log(Level.WARNING, "Dispose failed.", e); 321 } 322 } 323 if (null != this.loginContext) { 324 try { 325 this.loginContext.logout(); 326 } catch (LoginException le) { 327 LOGGER.log(Level.WARNING, "Logout failed.", le); 328 } 329 } 330 } 331 332 /** 333 * Performs authentication using the BASIC Auth mechanism. 334 * 335 * <p> 336 * Returns null if authentication failed or if the provided 337 * the auth scheme did not contain BASIC Auth data/token. 338 * </p> 339 * 340 * @return SpnegoPrincipal for the given auth scheme. 341 */ 342 private SpnegoPrincipal doBasicAuth(final SpnegoAuthScheme scheme 343 , final SpnegoHttpServletResponse resp) throws IOException { 344 345 final byte[] data = scheme.getToken(); 346 347 if (0 == data.length) { 348 LOGGER.finer("Basic Auth data was NULL."); 349 return null; 350 } 351 352 final String[] basicData = new String(data).split(":", 2); 353 354 // assert 355 if (basicData.length != 2) { 356 throw new IllegalArgumentException("Username/Password may" 357 + " have contained an invalid character. basicData.length=" 358 + basicData.length); 359 } 360 361 // substring to remove domain (if provided) 362 final String username = basicData[0].substring(basicData[0].indexOf('\\') + 1); 363 final String password = basicData[1]; 364 final CallbackHandler handler = SpnegoProvider.getUsernamePasswordHandler( 365 username, password); 366 367 SpnegoPrincipal principal = null; 368 369 try { 370 // assert 371 if (null == username || username.isEmpty()) { 372 throw new LoginException("Username is required."); 373 } 374 375 final LoginContext cntxt = new LoginContext(this.clientModuleName, handler); 376 377 // validate username/password by login/logout 378 cntxt.login(); 379 cntxt.logout(); 380 381 principal = new SpnegoPrincipal(username + '@' 382 + this.serverPrincipal.getRealm() 383 , KerberosPrincipal.KRB_NT_PRINCIPAL); 384 385 } catch (LoginException le) { 386 LOGGER.info(le.getMessage() + ": Login failed. username=" + username 387 + "; password.hashCode()=" + password.hashCode()); 388 389 resp.setHeader(Constants.AUTHN_HEADER, Constants.NEGOTIATE_HEADER); 390 resp.addHeader(Constants.AUTHN_HEADER, Constants.BASIC_HEADER 391 + " realm=\"" + this.serverPrincipal.getRealm() + '\"'); 392 393 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED, true); 394 } 395 396 return principal; 397 } 398 399 private SpnegoPrincipal doLocalhost() { 400 final String username = System.getProperty("user.name"); 401 402 if (null == username || username.isEmpty()) { 403 return new SpnegoPrincipal(this.serverPrincipal.getName() + '@' 404 + this.serverPrincipal.getRealm() 405 , this.serverPrincipal.getNameType()); 406 } else { 407 return new SpnegoPrincipal(username + '@' 408 + this.serverPrincipal.getRealm() 409 , KerberosPrincipal.KRB_NT_PRINCIPAL); 410 } 411 } 412 413 /** 414 * Performs authentication using the SPNEGO mechanism. 415 * 416 * <p> 417 * Returns null if authentication failed or if the provided 418 * the auth scheme did not contain the SPNEGO/GSS token. 419 * </p> 420 * 421 * @return SpnegoPrincipal for the given auth scheme. 422 */ 423 private SpnegoPrincipal doSpnegoAuth( 424 final SpnegoAuthScheme scheme, final SpnegoHttpServletResponse resp) 425 throws GSSException, IOException { 426 427 final String principal; 428 final byte[] gss = scheme.getToken(); 429 430 if (0 == gss.length) { 431 LOGGER.finer("GSS data was NULL."); 432 return null; 433 } 434 435 GSSContext context = null; 436 GSSCredential delegCred = null; 437 438 try { 439 byte[] token = null; 440 441 SpnegoAuthenticator.LOCK.lock(); 442 try { 443 context = SpnegoAuthenticator.MANAGER.createContext(this.serverCredentials); 444 token = context.acceptSecContext(gss, 0, gss.length); 445 } finally { 446 SpnegoAuthenticator.LOCK.unlock(); 447 } 448 449 if (null == token) { 450 LOGGER.finer("Token was NULL."); 451 return null; 452 } 453 454 resp.setHeader(Constants.AUTHN_HEADER, Constants.NEGOTIATE_HEADER 455 + ' ' + Base64.encode(token)); 456 457 if (!context.isEstablished()) { 458 LOGGER.fine("context not established"); 459 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED, true); 460 return null; 461 } 462 463 principal = context.getSrcName().toString(); 464 465 if (this.allowDelegation && context.getCredDelegState()) { 466 delegCred = context.getDelegCred(); 467 } 468 469 } finally { 470 if (null != context) { 471 SpnegoAuthenticator.LOCK.lock(); 472 try { 473 context.dispose(); 474 } finally { 475 SpnegoAuthenticator.LOCK.unlock(); 476 } 477 } 478 } 479 480 return new SpnegoPrincipal(principal, KerberosPrincipal.KRB_NT_PRINCIPAL, delegCred); 481 } 482 483 /** 484 * Returns true if HTTP request is from the same host (localhost). 485 * 486 * @param req servlet request 487 * @return true if HTTP request is from the same host (localhost) 488 */ 489 private boolean isLocalhost(final HttpServletRequest req) { 490 491 return req.getLocalAddr().equals(req.getRemoteAddr()); 492 } 493 }