1 /**
2 *
3 * Copyright 2003-2004 The Apache Software Foundation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.geronimo.security.realm.providers;
18
19 import java.io.Serializable;
20 import java.util.Map;
21 import java.util.HashMap;
22 import java.util.LinkedList;
23 import java.util.Iterator;
24 import javax.security.auth.Subject;
25 import javax.security.auth.callback.Callback;
26 import javax.security.auth.callback.CallbackHandler;
27 import javax.security.auth.callback.NameCallback;
28 import javax.security.auth.login.LoginException;
29 import javax.security.auth.login.FailedLoginException;
30 import javax.security.auth.spi.LoginModule;
31
32 /**
33 * Tracks the number of recent login failures for each user, and starts
34 * rejecting login attemps if the number of failures in a certain period for a
35 * particular user gets too high. The period, number of failures, and lockout
36 * duration are configurable, but default to 5 failures in 5 minutes cause all
37 * subsequent attemps to fail for 30 minutes.
38 *
39 * This module does not write any Principals into the Subject.
40 *
41 * To enable this login module, set your primary login module and any other
42 * login modules to REQUIRED or OPTIONAL, and list this module in last place,
43 * set to REQUISITE.
44 *
45 * The parameters used by this module are:
46 * <ul>
47 * <li><b>failureCount</b> - The number of failures to allow before subsequent
48 * login attempts automatically fail</li>
49 * <li><b>failurePeriodSecs</b> - The window of time the failures must occur
50 * in in order to cause the lockout</li>
51 * <li><b>lockoutDurationSecs</b> - The duration of a lockout caused by
52 * exceeding the failureCount in
53 * failurePeriodSecs.</li>
54 * </ul>
55 *
56 * @version $Rev: 355877 $ $Date: 2005-12-10 18:48:27 -0800 (Sat, 10 Dec 2005) $
57 */
58 public class RepeatedFailureLockoutLoginModule implements LoginModule {
59 public static final String FAILURE_COUNT_OPTION = "failureCount";
60 public static final String FAILURE_PERIOD_OPTION = "failurePeriodSecs";
61 public static final String LOCKOUT_DURATION_OPTION = "lockoutDurationSecs";
62 private static final HashMap userData = new HashMap();
63 private CallbackHandler handler;
64 private String username;
65 private int failureCount = 5;
66 private int failurePeriod = 5 * 60 * 1000;
67 private int lockoutDuration = 30 * 60 * 1000;
68
69 /**
70 * Reads the configuration settings for this module.
71 */
72 public void initialize(Subject subject, CallbackHandler callbackHandler,
73 Map sharedState, Map options) {
74 String fcString = (String) options.get(FAILURE_COUNT_OPTION);
75 if(fcString != null) {
76 fcString = fcString.trim();
77 if(!fcString.equals("")) {
78 failureCount = Integer.parseInt(fcString);
79 }
80 }
81 String fpString = (String) options.get(FAILURE_PERIOD_OPTION);
82 if(fpString != null) {
83 fpString = fpString.trim();
84 if(!fpString.equals("")) {
85 failurePeriod = Integer.parseInt(fpString) * 1000;
86 }
87 }
88 String ldString = (String) options.get(LOCKOUT_DURATION_OPTION);
89 if(ldString != null) {
90 ldString = ldString.trim();
91 if(!ldString.equals("")) {
92 lockoutDuration = Integer.parseInt(ldString) * 1000;
93 }
94 }
95 handler = callbackHandler;
96 }
97
98 /**
99 * 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;
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) {
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 }