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 }