View Javadoc

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; // 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 }