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 }