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.Map;
021    import java.util.HashMap;
022    import java.util.LinkedList;
023    import java.util.Iterator;
024    import javax.security.auth.Subject;
025    import javax.security.auth.callback.Callback;
026    import javax.security.auth.callback.CallbackHandler;
027    import javax.security.auth.callback.NameCallback;
028    import javax.security.auth.login.LoginException;
029    import javax.security.auth.login.FailedLoginException;
030    import javax.security.auth.spi.LoginModule;
031    
032    /**
033     * Tracks the number of recent login failures for each user, and starts
034     * rejecting login attemps if the number of failures in a certain period for a
035     * particular user gets too high.  The period, number of failures, and lockout
036     * duration are configurable, but default to 5 failures in 5 minutes cause all
037     * subsequent attemps to fail for 30 minutes.
038     *
039     * This module does not write any Principals into the Subject.
040     *
041     * To enable this login module, set your primary login module and any other
042     * login modules to REQUIRED or OPTIONAL, and list this module in last place,
043     * set to REQUISITE.
044     *
045     * The parameters used by this module are:
046     * <ul>
047     *   <li><b>failureCount</b> - The number of failures to allow before subsequent
048     *                             login attempts automatically fail</li>
049     *   <li><b>failurePeriodSecs</b> - The window of time the failures must occur
050     *                                 in in order to cause the lockout</li>
051     *   <li><b>lockoutDurationSecs</b> - The duration of a lockout caused by
052     *                                    exceeding the failureCount in
053     *                                    failurePeriodSecs.</li>
054     * </ul>
055     *
056     * This login module does not check credentials so it should never be able to cause a login to succeed.
057     * Therefore the lifecycle methods must return false to indicate success or throw a LoginException to indicate failure.
058     *
059     * @version $Rev: 565912 $ $Date: 2007-08-14 17:03:11 -0400 (Tue, 14 Aug 2007) $
060     */
061    public class RepeatedFailureLockoutLoginModule implements LoginModule {
062        public static final String FAILURE_COUNT_OPTION = "failureCount";
063        public static final String FAILURE_PERIOD_OPTION = "failurePeriodSecs";
064        public static final String LOCKOUT_DURATION_OPTION = "lockoutDurationSecs";
065        private static final HashMap<String, LoginHistory> userData = new HashMap<String, LoginHistory>();
066        private CallbackHandler handler;
067        private String username;
068        private int failureCount = 5;
069        private int failurePeriod = 5 * 60 * 1000;
070        private int lockoutDuration = 30 * 60 * 1000;
071    
072        /**
073         * Reads the configuration settings for this module.
074         */
075        public void initialize(Subject subject, CallbackHandler callbackHandler,
076                               Map sharedState, Map options) {
077            String fcString = (String) options.get(FAILURE_COUNT_OPTION);
078            if(fcString != null) {
079                fcString = fcString.trim();
080                if(!fcString.equals("")) {
081                    failureCount = Integer.parseInt(fcString);
082                }
083            }
084            String fpString = (String) options.get(FAILURE_PERIOD_OPTION);
085            if(fpString != null) {
086                fpString = fpString.trim();
087                if(!fpString.equals("")) {
088                    failurePeriod = Integer.parseInt(fpString) * 1000;
089                }
090            }
091            String ldString = (String) options.get(LOCKOUT_DURATION_OPTION);
092            if(ldString != null) {
093                ldString = ldString.trim();
094                if(!ldString.equals("")) {
095                    lockoutDuration = Integer.parseInt(ldString) * 1000;
096                }
097            }
098            handler = callbackHandler;
099        }
100    
101        /**
102         * Checks whether the user should be or has been locked out.
103         */
104        public boolean login() throws LoginException {
105            NameCallback user = new NameCallback("User name:");
106            Callback[] callbacks = new Callback[]{user};
107            try {
108                handler.handle(callbacks);
109            } catch (Exception e) {
110                throw (LoginException)new LoginException("Unable to process callback: "+e.getMessage()).initCause(e);
111            }
112            if(callbacks.length != 1) {
113                throw new IllegalStateException("Number of callbacks changed by server!");
114            }
115            user = (NameCallback) callbacks[0];
116            username = user.getName();
117            if(username != null) {
118                LoginHistory history;
119                synchronized (userData) {
120                    history = userData.get(username);
121                }
122                if(history != null && !history.isLoginAllowed(lockoutDuration, failurePeriod, failureCount)) {
123                    username = null;
124                    throw new FailedLoginException("Maximum login failures exceeded; try again later");
125                }
126            }
127            return false;
128        }
129    
130        /**
131         * This module does nothing if a login succeeds.
132         */
133        public boolean commit() throws LoginException {
134            return false;
135        }
136    
137        /**
138         * Notes that (and when) a login failure occured, used to calculate
139         * whether the user should be locked out.
140         */
141        public boolean abort() throws LoginException {
142            if(username != null) { //work around initial "fake" login
143                LoginHistory history;
144                synchronized (userData) {
145                    history = userData.get(username);
146                    if(history == null) {
147                        history = new LoginHistory(username);
148                        userData.put(username, history);
149                    }
150                }
151                history.addFailure();
152                username = null;
153            }
154            return false;
155         }
156    
157        /**
158         * This module does nothing on a logout.
159         */
160        public boolean logout() throws LoginException {
161            username = null;
162            handler = null;
163            return false;
164        }
165    
166        /**
167         * Tracks failure attempts for a user, and calculates lockout
168         * status and expiry, etc.
169         */
170        private static class LoginHistory implements Serializable {
171            private static final long serialVersionUID = 7792298296084531182L;
172    
173            private String user;
174            private LinkedList<Long> data = new LinkedList<Long>();
175            private long lockExpires = -1;
176    
177            public LoginHistory(String user) {
178                this.user = user;
179            }
180    
181            public String getUser() {
182                return user;
183            }
184    
185            /**
186             * Cleans up the failure history and then calculates whether this user
187             * is locked out or not.
188             */
189            public synchronized boolean isLoginAllowed(int lockoutLengthMillis, int failureAgeMillis, int maxFailures) {
190                long now = System.currentTimeMillis();
191                cleanup(now - failureAgeMillis);
192                if(lockExpires > now) {
193                    return false;
194                }
195                if(data.size() >= maxFailures) {
196                    lockExpires = data.getLast() + lockoutLengthMillis;
197                    if(lockExpires > now) {
198                        return false;
199                    }
200                }
201                return true;
202            }
203    
204            /**
205             * Notes that a failure occured.
206             */
207            public synchronized void addFailure() {
208                data.add(System.currentTimeMillis());
209            }
210    
211            /**
212             * Cleans up all failure records outside the window of time we care
213             * about.
214             */
215            public synchronized void cleanup(long ignoreOlderThan) {
216                for (Iterator it = data.iterator(); it.hasNext();) {
217                    Long time = (Long) it.next();
218                    if(time < ignoreOlderThan) {
219                        it.remove();
220                    } else {
221                        break;
222                    }
223                }
224            }
225        }
226    }