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.File; 022import java.io.FileNotFoundException; 023import java.net.URI; 024import java.net.URISyntaxException; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.List; 028import java.util.Map; 029import java.util.logging.Level; 030import java.util.logging.Logger; 031 032import javax.security.auth.login.AppConfigurationEntry; 033import javax.security.auth.login.Configuration; 034import javax.servlet.FilterConfig; 035 036import net.sourceforge.spnego.SpnegoHttpFilter.Constants; 037 038/** 039 * Class that applies/enforces web.xml init params. 040 * 041 * <p>These properties are set in the servlet's init params 042 * in the web.xml file.</> 043 * 044 * <p>This class also validates if a keyTab should be used 045 * and if all of the LoginModule options have been set.</p> 046 * 047 * <p> 048 * To see a working example and instructions on how to use a keytab, take 049 * a look at the <a href="http://spnego.sourceforge.net/server_keytab.html" 050 * target="_blank">creating a server keytab</a> example. 051 * </p> 052 * 053 * <p>The class should be used as a Singleton:<br /> 054 * <code> 055 * SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(filter); 056 * </code> 057 * </p> 058 * 059 * <p>See an example web.xml configuration in the 060 * <a href="http://spnego.sourceforge.net/spnego_tomcat.html" 061 * target="_blank">installing on tomcat</a> documentation. 062 * </p> 063 * 064 * @author Darwin V. Felix 065 * 066 */ 067public final class SpnegoFilterConfig { // NOPMD 068 069 private static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME); 070 071 private static final String MISSING_PROPERTY = 072 "Servlet Filter init param(s) in web.xml missing: "; 073 074 private static transient SpnegoFilterConfig instance = null; 075 076 /** true if Basic auth should be offered. */ 077 private transient boolean allowBasic = false; 078 079 /** true if server should support credential delegation requests. */ 080 private transient boolean allowDelegation = false; 081 082 /** true if request from localhost should not be authenticated. */ 083 private transient boolean allowLocalhost = true; 084 085 /** true if non-ssl for basic auth is allowed. */ 086 private transient boolean allowUnsecure = true; 087 088 /** true if all req. login module options set. */ 089 private transient boolean canUseKeyTab = false; 090 091 /** name of the client login module. */ 092 private transient String clientLoginModule = null; 093 094 /** url directory path(s) that should NOT undergo authentication. */ 095 private transient String excludeDirs = null; 096 097 /** password to domain account. */ 098 private transient String password = null; 099 100 /** true if instead of err on ntlm token, prompt for username/pass. */ 101 private transient boolean promptNtlm = false; 102 103 /** name of the server login module. */ 104 private transient String serverLoginModule = null; 105 106 /** domain account to use for pre-authentication. */ 107 private transient String username = null; 108 109 private SpnegoFilterConfig() { 110 // default private 111 } 112 113 /** 114 * Class is a Singleton. Use the static getInstance() method. 115 */ 116 private SpnegoFilterConfig(final FilterConfig config) throws FileNotFoundException 117 , URISyntaxException { 118 119 // specify logging level 120 setLogLevel(config.getInitParameter(Constants.LOGGER_LEVEL)); 121 122 // check if exists 123 assert loginConfExists(config.getInitParameter(Constants.LOGIN_CONF)); 124 125 // specify krb5 conf as a System property 126 if (null == config.getInitParameter(Constants.KRB5_CONF)) { 127 throw new IllegalArgumentException( 128 SpnegoFilterConfig.MISSING_PROPERTY + Constants.KRB5_CONF); 129 } else { 130 System.setProperty("java.security.krb5.conf" 131 , config.getInitParameter(Constants.KRB5_CONF)); 132 } 133 134 // specify login conf as a System property 135 if (null == config.getInitParameter(Constants.LOGIN_CONF)) { 136 throw new IllegalArgumentException( 137 SpnegoFilterConfig.MISSING_PROPERTY + Constants.LOGIN_CONF); 138 } else { 139 System.setProperty("java.security.auth.login.config" 140 , config.getInitParameter(Constants.LOGIN_CONF)); 141 } 142 143 // check if exists and no options specified 144 doClientModule(config.getInitParameter(Constants.CLIENT_MODULE)); 145 146 // determine if all req. met to use keyTab 147 doServerModule(config.getInitParameter(Constants.SERVER_MODULE)); 148 149 // if username/password provided, don't use key tab 150 setUsernamePassword(config.getInitParameter(Constants.PREAUTH_USERNAME) 151 , config.getInitParameter(Constants.PREAUTH_PASSWORD)); 152 153 // determine if we should support Basic Authentication 154 setBasicSupport(config.getInitParameter(Constants.ALLOW_BASIC) 155 , config.getInitParameter(Constants.ALLOW_UNSEC_BASIC)); 156 157 // determine if we should Basic Auth prompt if rec. NTLM token 158 setNtlmSupport(config.getInitParameter(Constants.PROMPT_NTLM)); 159 160 // requests from localhost will not be authenticated against the KDC 161 if (null != config.getInitParameter(Constants.ALLOW_LOCALHOST)) { 162 this.allowLocalhost = 163 Boolean.parseBoolean(config.getInitParameter(Constants.ALLOW_LOCALHOST)); 164 } 165 166 // determine if the server supports credential delegation 167 if (null != config.getInitParameter(Constants.ALLOW_DELEGATION)) { 168 this.allowDelegation = 169 Boolean.parseBoolean(config.getInitParameter(Constants.ALLOW_DELEGATION)); 170 } 171 172 // determine if a url path(s) should NOT undergo authentication 173 this.excludeDirs = config.getInitParameter(Constants.EXCLUDE_DIRS); 174 } 175 176 private void doClientModule(final String moduleName) { 177 178 assert moduleExists("client", moduleName); 179 180 this.clientLoginModule = moduleName; 181 182 // client must not have any options 183 184 // confirm that runtime loaded the login file 185 final Configuration config = Configuration.getConfiguration(); 186 187 // we only expect one entry 188 final AppConfigurationEntry entry = config.getAppConfigurationEntry(moduleName)[0]; 189 190 // get login module options 191 final Map<String, ?> opt = entry.getOptions(); 192 193 // assert 194 if (!opt.isEmpty()) { 195 for (Map.Entry<String, ?> option : opt.entrySet()) { 196 // do not allow client modules to have any options 197 // unless they are jboss options 198 if (!option.getKey().startsWith("jboss")) { 199 throw new UnsupportedOperationException("Login Module for client must not " 200 + "specify any options: " + opt.size() 201 + "; moduleName=" + moduleName 202 + "; options=" + opt.toString()); 203 } 204 } 205 } 206 } 207 208 /** 209 * Set the canUseKeyTab flag by determining if all LoginModule options 210 * have been set. 211 * 212 * <pre> 213 * my-spnego-login-module { 214 * com.sun.security.auth.module.Krb5LoginModule 215 * required 216 * storeKey=true 217 * useKeyTab=true 218 * keyTab="file:///C:/my_path/my_file.keytab" 219 * principal="my_preauth_account"; 220 * }; 221 * </pre> 222 * 223 * @param moduleName 224 */ 225 private void doServerModule(final String moduleName) { 226 227 assert moduleExists("server", moduleName); 228 229 this.serverLoginModule = moduleName; 230 231 // confirm that runtime loaded the login file 232 final Configuration config = Configuration.getConfiguration(); 233 234 // we only expect one entry 235 final AppConfigurationEntry entry = config.getAppConfigurationEntry(moduleName)[0]; 236 237 // get login module options 238 final Map<String, ?> opt = entry.getOptions(); 239 240 // storeKey must be set to true 241 if (opt.containsKey("storeKey")) { 242 final Object store = opt.get("storeKey"); 243 if (null == store || !Boolean.parseBoolean((String) store)) { 244 throw new UnsupportedOperationException("Login Module for server " 245 + "must have storeKey option in login file set to true."); 246 } 247 } else { 248 throw new UnsupportedOperationException("Login Module for server does " 249 + "not have the storeKey option defined in login file."); 250 } 251 252 if (opt.containsKey("useKeyTab") 253 && opt.containsKey("principal") 254 && opt.containsKey("keyTab")) { 255 256 this.canUseKeyTab = true; 257 } else { 258 this.canUseKeyTab = false; 259 } 260 } 261 262 /** 263 * Returns true if a client sends an NTLM token and the 264 * filter should ask client for a Basic Auth token instead. 265 * 266 * @return true if client should be prompted for Basic Auth 267 */ 268 boolean downgradeNtlm() { 269 return this.promptNtlm; 270 } 271 272 /** 273 * Return the value defined in the servlet's init params 274 * in the web.xml file. 275 * 276 * @return the name of the login module for the client 277 */ 278 String getClientLoginModule() { 279 return this.clientLoginModule; 280 } 281 282 /** 283 * Return the value defined in the servlet's init params 284 * in the web.xml file as a List object. 285 * 286 * @return a List of directories to exclude 287 */ 288 List<String> getExcludeDirs() { 289 if (null == this.excludeDirs || this.excludeDirs.isEmpty()) { 290 return Collections.emptyList(); 291 } else { 292 return SpnegoFilterConfig.split(this.excludeDirs); 293 } 294 } 295 296 /** 297 * Return the password to the pre-authentication domain account. 298 * 299 * @return password of pre-auth domain account 300 */ 301 String getPreauthPassword() { 302 return this.password; 303 } 304 305 /** 306 * Return the name of the pre-authentication domain account. 307 * 308 * @return name of pre-auth domain account 309 */ 310 String getPreauthUsername() { 311 return this.username; 312 } 313 314 /** 315 * Return the value defined in the servlet's init params 316 * in the web.xml file. 317 * 318 * @return the name of the login module for the server 319 */ 320 String getServerLoginModule() { 321 return this.serverLoginModule; 322 } 323 324 /** 325 * Returns the instance of the servlet's config parameters. 326 * 327 * @param config FilterConfi from servlet's init method 328 * @return the instance of that represent the init params 329 * @throws FileNotFoundException if login conf file not found 330 * @throws URISyntaxException if path to login conf is bad 331 */ 332 public static SpnegoFilterConfig getInstance(final FilterConfig config) 333 throws FileNotFoundException, URISyntaxException { 334 335 synchronized (SpnegoFilterConfig.class) { 336 if (null == SpnegoFilterConfig.instance) { 337 SpnegoFilterConfig.instance = new SpnegoFilterConfig(config); 338 } 339 } 340 341 return SpnegoFilterConfig.instance; 342 } 343 344 /** 345 * Returns true if Basic Authentication is allowed. 346 * 347 * @return true if Basic Auth is allowed 348 */ 349 boolean isBasicAllowed() { 350 return this.allowBasic; 351 } 352 353 /** 354 * Returns true if the server should support credential delegation requests. 355 * 356 * @return true if server supports credential delegation 357 */ 358 boolean isDelegationAllowed() { 359 return this.allowDelegation; 360 } 361 362 /** 363 * Returns true if requests from localhost are allowed. 364 * 365 * @return true if requests from localhost are allowed 366 */ 367 boolean isLocalhostAllowed() { 368 return this.allowLocalhost; 369 } 370 371 /** 372 * Returns true if SSL/TLS is required. 373 * 374 * @return true if SSL/TLS is required 375 */ 376 boolean isUnsecureAllowed() { 377 return this.allowUnsecure; 378 } 379 380 private boolean loginConfExists(final String loginconf) 381 throws FileNotFoundException, URISyntaxException { 382 383 // confirm login.conf file exists 384 if (null == loginconf || loginconf.isEmpty()) { 385 throw new FileNotFoundException("Must provide a login.conf file."); 386 } else { 387 final File file = new File(new URI(loginconf)); 388 if (!file.exists()) { 389 throw new FileNotFoundException(loginconf); 390 } 391 } 392 393 return true; 394 } 395 396 private boolean moduleExists(final String side, final String moduleName) { 397 398 // confirm that runtime loaded the login file 399 final Configuration config = Configuration.getConfiguration(); 400 401 // we only expect one entry 402 final AppConfigurationEntry[] entry = config.getAppConfigurationEntry(moduleName); 403 404 // confirm that the module name exists in the file 405 if (null == entry) { 406 throw new IllegalArgumentException("The " + side + " module name " 407 + "was not found in the login file: " + moduleName); 408 } 409 410 // confirm that the login module class was defined 411 if (0 == entry.length) { 412 throw new IllegalArgumentException("The " + side + " module name " 413 + "exists but login module class not defined: " + moduleName); 414 } 415 416 // confirm that only one login module class specified 417 if (entry.length > 1) { 418 throw new IllegalArgumentException("Only one login module class " 419 + "is supported for the " + side + " module: " + entry.length); 420 } 421 422 // confirm class name is "com.sun.security.auth.module.Krb5LoginModule" 423 if (!entry[0].getLoginModuleName().equals( 424 "com.sun.security.auth.module.Krb5LoginModule")) { 425 throw new UnsupportedOperationException("Login module class not " 426 + "supported: " + entry[0].getLoginModuleName()); 427 } 428 429 // confirm Control Flag is specified as REQUIRED 430 if (!entry[0].getControlFlag().equals( 431 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED)) { 432 throw new UnsupportedOperationException("Control Flag must " 433 + "have a value of REQUIRED: " + entry[0].getControlFlag()); 434 } 435 436 return true; 437 } 438 439 /** 440 * Specify if Basic authentication is allowed and if un-secure/non-ssl 441 * Basic should be allowed. 442 * 443 * @param basic true if basic is allowed 444 * @param unsecure true if un-secure basic is allowed 445 */ 446 private void setBasicSupport(final String basic, final String unsecure) { 447 if (null == basic) { 448 throw new IllegalArgumentException( 449 SpnegoFilterConfig.MISSING_PROPERTY + Constants.ALLOW_BASIC); 450 } 451 452 if (null == unsecure) { 453 throw new IllegalArgumentException( 454 SpnegoFilterConfig.MISSING_PROPERTY + Constants.ALLOW_UNSEC_BASIC); 455 } 456 457 this.allowBasic = Boolean.parseBoolean(basic); 458 this.allowUnsecure = Boolean.parseBoolean(unsecure); 459 } 460 461 /** 462 * Specify the logging level. 463 * 464 * @param level logging level 465 */ 466 private void setLogLevel(final String level) { 467 if (null != level) { 468 switch (Integer.parseInt(level)) { 469 case 1: 470 LOGGER.setLevel(Level.FINEST); 471 break; 472 case 2: 473 LOGGER.setLevel(Level.FINER); 474 break; 475 case 3: 476 LOGGER.setLevel(Level.FINE); 477 break; 478 case 4: 479 LOGGER.setLevel(Level.CONFIG); 480 break; 481 case 6: 482 LOGGER.setLevel(Level.WARNING); 483 break; 484 case 7: 485 LOGGER.setLevel(Level.SEVERE); 486 break; 487 default : 488 LOGGER.setLevel(Level.INFO); 489 break; 490 } 491 } 492 } 493 494 /** 495 * If request contains NTLM token, specify if a 401 should 496 * be sent back to client with Basic Auth as the only 497 * available authentication scheme. 498 * 499 * @param ntlm true/false 500 */ 501 private void setNtlmSupport(final String ntlm) { 502 if (null == ntlm) { 503 throw new IllegalArgumentException( 504 SpnegoFilterConfig.MISSING_PROPERTY + Constants.PROMPT_NTLM); 505 } 506 507 final boolean downgradeNtlm = Boolean.parseBoolean(ntlm); 508 509 if (!this.allowBasic && downgradeNtlm) { 510 throw new IllegalArgumentException("If prompt ntlm is true, then " 511 + "allow basic auth must also be true."); 512 } 513 514 this.promptNtlm = downgradeNtlm; 515 } 516 517 /** 518 * Set the username and password if specified in web.xml's init params. 519 * 520 * @param usr domain account 521 * @param psswrd the password to the domain account 522 * @throws IllegalArgumentException if user/pass AND keyTab set 523 */ 524 private void setUsernamePassword(final String usr, final String psswrd) { 525 boolean mustUseKtab = false; 526 527 if (null == usr) { 528 this.username = ""; 529 } else { 530 this.username = usr; 531 } 532 533 if (null == psswrd) { 534 this.password = ""; 535 } else { 536 this.password = psswrd; 537 } 538 539 if (this.username.isEmpty() || this.password.isEmpty()) { 540 mustUseKtab = true; 541 } 542 543 if (mustUseKtab && !this.canUseKeyTab) { 544 throw new IllegalArgumentException("Must specify a username " 545 + "and password or a keyTab."); 546 } 547 } 548 549 /** 550 * Returns true if LoginContext should use keyTab. 551 * 552 * @return true if LoginContext should use keyTab. 553 */ 554 boolean useKeyTab() { 555 return this.canUseKeyTab && this.username.isEmpty() && this.password.isEmpty(); 556 } 557 558 private static String clean(final String path) { 559 560 // assert - more than one char (we do not support ROOT) and no wild card 561 if (path.length() < 2 || path.contains("*")) { 562 throw new IllegalArgumentException( 563 "Invalid exclude.dirs pattern or char(s): " + path); 564 } 565 566 // ensure that it ends with the slash character 567 final String tmp; 568 if (path.endsWith("/")) { 569 tmp = path; 570 } else { 571 tmp = path + "/"; 572 } 573 574 // we want to include the slash character 575 return tmp.substring(0, tmp.lastIndexOf('/') + 1); 576 } 577 578 private static List<String> split(final String dirs) { 579 final List<String> list = new ArrayList<String>(); 580 581 for (String dir : dirs.split(",")) { 582 list.add(SpnegoFilterConfig.clean(dir.trim())); 583 } 584 585 return list; 586 } 587 588 @Override 589 public String toString() { 590 final StringBuilder buff = new StringBuilder(); 591 592 buff.append("allowBasic=" + this.allowBasic 593 + "; allowUnsecure=" + this.allowUnsecure 594 + "; canUseKeyTab=" + this.canUseKeyTab 595 + "; clientLoginModule=" + this.clientLoginModule 596 + "; serverLoginModule=" + this.serverLoginModule); 597 598 return buff.toString(); 599 } 600}