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