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 }