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    }