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 }