001    /**
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package org.apache.geronimo.security.realm.providers;
018    
019    import java.io.Serializable;
020    import java.util.Arrays;
021    import java.util.Collections;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.HashMap;
025    import java.util.LinkedList;
026    import java.util.Iterator;
027    import javax.security.auth.Subject;
028    import javax.security.auth.callback.Callback;
029    import javax.security.auth.callback.CallbackHandler;
030    import javax.security.auth.callback.NameCallback;
031    import javax.security.auth.login.LoginException;
032    import javax.security.auth.login.FailedLoginException;
033    import javax.security.auth.spi.LoginModule;
034    
035    import org.apache.commons.logging.Log;
036    import org.apache.commons.logging.LogFactory;
037    import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
038    import org.apache.geronimo.security.jaas.WrappingLoginModule;
039    
040    /**
041     * Tracks the number of recent login failures for each user, and starts
042     * rejecting login attemps if the number of failures in a certain period for a
043     * particular user gets too high.  The period, number of failures, and lockout
044     * duration are configurable, but default to 5 failures in 5 minutes cause all
045     * subsequent attemps to fail for 30 minutes.
046     *
047     * This module does not write any Principals into the Subject.
048     *
049     * To enable this login module, set your primary login module and any other
050     * login modules to REQUIRED or OPTIONAL, and list this module in last place,
051     * set to REQUISITE.
052     *
053     * The parameters used by this module are:
054     * <ul>
055     *   <li><b>failureCount</b> - The number of failures to allow before subsequent
056     *                             login attempts automatically fail</li>
057     *   <li><b>failurePeriodSecs</b> - The window of time the failures must occur
058     *                                 in in order to cause the lockout</li>
059     *   <li><b>lockoutDurationSecs</b> - The duration of a lockout caused by
060     *                                    exceeding the failureCount in
061     *                                    failurePeriodSecs.</li>
062     * </ul>
063     *
064     * This login module does not check credentials so it should never be able to cause a login to succeed.
065     * Therefore the lifecycle methods must return false to indicate success or throw a LoginException to indicate failure.
066     *
067     * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
068     */
069    public class RepeatedFailureLockoutLoginModule implements LoginModule {
070        private static Log log = LogFactory.getLog(RepeatedFailureLockoutLoginModule.class);
071        
072        public static final String FAILURE_COUNT_OPTION = "failureCount";
073        public static final String FAILURE_PERIOD_OPTION = "failurePeriodSecs";
074        public static final String LOCKOUT_DURATION_OPTION = "lockoutDurationSecs";
075        public final static List<String> supportedOptions = Collections.unmodifiableList(Arrays.asList(FAILURE_COUNT_OPTION, FAILURE_PERIOD_OPTION, LOCKOUT_DURATION_OPTION));
076        
077        private static final HashMap<String, LoginHistory> userData = new HashMap<String, LoginHistory>();
078        private CallbackHandler handler;
079        private String username;
080        private int failureCount = 5;
081        private int failurePeriod = 5 * 60 * 1000;
082        private int lockoutDuration = 30 * 60 * 1000;
083    
084        /**
085         * Reads the configuration settings for this module.
086         */
087        public void initialize(Subject subject, CallbackHandler callbackHandler,
088                               Map sharedState, Map options) {
089            for(Object option: options.keySet()) {
090                if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option)
091                        && !WrappingLoginModule.supportedOptions.contains(option)) {
092                    log.warn("Ignoring option: "+option+". Not supported.");
093                }
094            }
095            String fcString = (String) options.get(FAILURE_COUNT_OPTION);
096            if(fcString != null) {
097                fcString = fcString.trim();
098                if(!fcString.equals("")) {
099                    failureCount = Integer.parseInt(fcString);
100                }
101            }
102            String fpString = (String) options.get(FAILURE_PERIOD_OPTION);
103            if(fpString != null) {
104                fpString = fpString.trim();
105                if(!fpString.equals("")) {
106                    failurePeriod = Integer.parseInt(fpString) * 1000;
107                }
108            }
109            String ldString = (String) options.get(LOCKOUT_DURATION_OPTION);
110            if(ldString != null) {
111                ldString = ldString.trim();
112                if(!ldString.equals("")) {
113                    lockoutDuration = Integer.parseInt(ldString) * 1000;
114                }
115            }
116            handler = callbackHandler;
117        }
118    
119        /**
120         * Checks whether the user should be or has been locked out.
121         */
122        public boolean login() throws LoginException {
123            NameCallback user = new NameCallback("User name:");
124            Callback[] callbacks = new Callback[]{user};
125            try {
126                handler.handle(callbacks);
127            } catch (Exception e) {
128                throw (LoginException)new LoginException("Unable to process callback: "+e.getMessage()).initCause(e);
129            }
130            if(callbacks.length != 1) {
131                throw new IllegalStateException("Number of callbacks changed by server!");
132            }
133            user = (NameCallback) callbacks[0];
134            username = user.getName();
135            if(username != null) {
136                LoginHistory history;
137                synchronized (userData) {
138                    history = userData.get(username);
139                }
140                if(history != null && !history.isLoginAllowed(lockoutDuration, failurePeriod, failureCount)) {
141                    username = null;
142                    throw new FailedLoginException("Maximum login failures exceeded; try again later");
143                }
144            }
145            return false;
146        }
147    
148        /**
149         * This module does nothing if a login succeeds.
150         */
151        public boolean commit() throws LoginException {
152            return false;
153        }
154    
155        /**
156         * Notes that (and when) a login failure occured, used to calculate
157         * whether the user should be locked out.
158         */
159        public boolean abort() throws LoginException {
160            if(username != null) { //work around initial "fake" login
161                LoginHistory history;
162                synchronized (userData) {
163                    history = userData.get(username);
164                    if(history == null) {
165                        history = new LoginHistory(username);
166                        userData.put(username, history);
167                    }
168                }
169                history.addFailure();
170                username = null;
171            }
172            return false;
173         }
174    
175        /**
176         * This module does nothing on a logout.
177         */
178        public boolean logout() throws LoginException {
179            username = null;
180            return false;
181        }
182    
183        /**
184         * Tracks failure attempts for a user, and calculates lockout
185         * status and expiry, etc.
186         */
187        private static class LoginHistory implements Serializable {
188            private static final long serialVersionUID = 7792298296084531182L;
189    
190            private String user;
191            private LinkedList<Long> data = new LinkedList<Long>();
192            private long lockExpires = -1;
193    
194            public LoginHistory(String user) {
195                this.user = user;
196            }
197    
198            public String getUser() {
199                return user;
200            }
201    
202            /**
203             * Cleans up the failure history and then calculates whether this user
204             * is locked out or not.
205             */
206            public synchronized boolean isLoginAllowed(int lockoutLengthMillis, int failureAgeMillis, int maxFailures) {
207                long now = System.currentTimeMillis();
208                cleanup(now - failureAgeMillis);
209                if(lockExpires > now) {
210                    return false;
211                }
212                if(data.size() >= maxFailures) {
213                    lockExpires = data.getLast() + lockoutLengthMillis;
214                    if(lockExpires > now) {
215                        return false;
216                    }
217                }
218                return true;
219            }
220    
221            /**
222             * Notes that a failure occured.
223             */
224            public synchronized void addFailure() {
225                data.add(System.currentTimeMillis());
226            }
227    
228            /**
229             * Cleans up all failure records outside the window of time we care
230             * about.
231             */
232            public synchronized void cleanup(long ignoreOlderThan) {
233                for (Iterator it = data.iterator(); it.hasNext();) {
234                    Long time = (Long) it.next();
235                    if(time < ignoreOlderThan) {
236                        it.remove();
237                    } else {
238                        break;
239                    }
240                }
241            }
242        }
243    }