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