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 }