001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  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     * @version $Rev: 355877 $ $Date: 2005-12-10 18:48:27 -0800 (Sat, 10 Dec 2005) $
057     */
058    public class RepeatedFailureLockoutLoginModule implements LoginModule {
059        public static final String FAILURE_COUNT_OPTION = "failureCount";
060        public static final String FAILURE_PERIOD_OPTION = "failurePeriodSecs";
061        public static final String LOCKOUT_DURATION_OPTION = "lockoutDurationSecs";
062        private static final HashMap userData = new HashMap();
063        private CallbackHandler handler;
064        private String username;
065        private int failureCount = 5;
066        private int failurePeriod = 5 * 60 * 1000;
067        private int lockoutDuration = 30 * 60 * 1000;
068    
069        /**
070         * Reads the configuration settings for this module.
071         */
072        public void initialize(Subject subject, CallbackHandler callbackHandler,
073                               Map sharedState, Map options) {
074            String fcString = (String) options.get(FAILURE_COUNT_OPTION);
075            if(fcString != null) {
076                fcString = fcString.trim();
077                if(!fcString.equals("")) {
078                    failureCount = Integer.parseInt(fcString);
079                }
080            }
081            String fpString = (String) options.get(FAILURE_PERIOD_OPTION);
082            if(fpString != null) {
083                fpString = fpString.trim();
084                if(!fpString.equals("")) {
085                    failurePeriod = Integer.parseInt(fpString) * 1000;
086                }
087            }
088            String ldString = (String) options.get(LOCKOUT_DURATION_OPTION);
089            if(ldString != null) {
090                ldString = ldString.trim();
091                if(!ldString.equals("")) {
092                    lockoutDuration = Integer.parseInt(ldString) * 1000;
093                }
094            }
095            handler = callbackHandler;
096        }
097    
098        /**
099         * Checks whether the user should be or has been locked out.
100         */
101        public boolean login() throws LoginException {
102            NameCallback user = new NameCallback("User name:");
103            Callback[] callbacks = new Callback[]{user};
104            try {
105                handler.handle(callbacks);
106            } catch (Exception e) {
107                throw new LoginException("Unable to process callback: "+e);
108            }
109            if(callbacks.length != 1) {
110                throw new IllegalStateException("Number of callbacks changed by server!");
111            }
112            user = (NameCallback) callbacks[0];
113            username = user.getName();
114            if(username != null) {
115                LoginHistory history;
116                synchronized (userData) {
117                    history = (LoginHistory) userData.get(username);
118                }
119                if(history != null && !history.isLoginAllowed(lockoutDuration, failurePeriod, failureCount)) {
120                    username = null;
121                    throw new FailedLoginException("Maximum login failures exceeded; try again later");
122                }
123            } else {
124                return false; // it's a fake login, ignore this module
125            }
126            return true;
127        }
128    
129        /**
130         * This module does nothing if a login succeeds.
131         */
132        public boolean commit() throws LoginException {
133            return username != null;
134        }
135    
136        /**
137         * Notes that (and when) a login failure occured, used to calculate
138         * whether the user should be locked out.
139         */
140        public boolean abort() throws LoginException {
141            if(username != null) { //work around initial "fake" login
142                LoginHistory history;
143                synchronized (userData) {
144                    history = (LoginHistory) userData.get(username);
145                    if(history == null) {
146                        history = new LoginHistory(username);
147                        userData.put(username, history);
148                    }
149                }
150                history.addFailure();
151                username = null;
152                return true;
153            } else {
154                return false;
155            }
156        }
157    
158        /**
159         * This module does nothing on a logout.
160         */
161        public boolean logout() throws LoginException {
162            username = null;
163            handler = null;
164            return true;
165        }
166    
167        /**
168         * Tracks failure attempts for a user, and calculates lockout
169         * status and expiry, etc.
170         */
171        private static class LoginHistory implements Serializable {
172            private String user;
173            private LinkedList data = new LinkedList();
174            private long lockExpires = -1;
175    
176            public LoginHistory(String user) {
177                this.user = user;
178            }
179    
180            public String getUser() {
181                return user;
182            }
183    
184            /**
185             * Cleans up the failure history and then calculates whether this user
186             * is locked out or not.
187             */
188            public synchronized boolean isLoginAllowed(int lockoutLengthMillis, int failureAgeMillis, int maxFailures) {
189                long now = System.currentTimeMillis();
190                cleanup(now - failureAgeMillis);
191                if(lockExpires > now) {
192                    return false;
193                }
194                if(data.size() >= maxFailures) {
195                    lockExpires = ((Long)data.getLast()).longValue() + lockoutLengthMillis;
196                    if(lockExpires > now) {
197                        return false;
198                    }
199                }
200                return true;
201            }
202    
203            /**
204             * Notes that a failure occured.
205             */
206            public synchronized void addFailure() {
207                data.add(new Long(System.currentTimeMillis()));
208            }
209    
210            /**
211             * Cleans up all failure records outside the window of time we care
212             * about.
213             */
214            public synchronized void cleanup(long ignoreOlderThan) {
215                for (Iterator it = data.iterator(); it.hasNext();) {
216                    Long time = (Long) it.next();
217                    if(time.longValue() < ignoreOlderThan) {
218                        it.remove();
219                    } else {
220                        break;
221                    }
222                }
223            }
224        }
225    }