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.Arrays; 021 import java.util.Collections; 022 import java.util.List; 023 import java.util.Map; 024 import java.util.HashMap; 025 import java.util.LinkedList; 026 import java.util.Iterator; 027 import javax.security.auth.Subject; 028 import javax.security.auth.callback.Callback; 029 import javax.security.auth.callback.CallbackHandler; 030 import javax.security.auth.callback.NameCallback; 031 import javax.security.auth.login.LoginException; 032 import javax.security.auth.login.FailedLoginException; 033 import javax.security.auth.spi.LoginModule; 034 035 import org.apache.commons.logging.Log; 036 import org.apache.commons.logging.LogFactory; 037 import org.apache.geronimo.security.jaas.JaasLoginModuleUse; 038 import org.apache.geronimo.security.jaas.WrappingLoginModule; 039 040 /** 041 * Tracks the number of recent login failures for each user, and starts 042 * rejecting login attemps if the number of failures in a certain period for a 043 * particular user gets too high. The period, number of failures, and lockout 044 * duration are configurable, but default to 5 failures in 5 minutes cause all 045 * subsequent attemps to fail for 30 minutes. 046 * 047 * This module does not write any Principals into the Subject. 048 * 049 * To enable this login module, set your primary login module and any other 050 * login modules to REQUIRED or OPTIONAL, and list this module in last place, 051 * set to REQUISITE. 052 * 053 * The parameters used by this module are: 054 * <ul> 055 * <li><b>failureCount</b> - The number of failures to allow before subsequent 056 * login attempts automatically fail</li> 057 * <li><b>failurePeriodSecs</b> - The window of time the failures must occur 058 * in in order to cause the lockout</li> 059 * <li><b>lockoutDurationSecs</b> - The duration of a lockout caused by 060 * exceeding the failureCount in 061 * failurePeriodSecs.</li> 062 * </ul> 063 * 064 * This login module does not check credentials so it should never be able to cause a login to succeed. 065 * Therefore the lifecycle methods must return false to indicate success or throw a LoginException to indicate failure. 066 * 067 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $ 068 */ 069 public class RepeatedFailureLockoutLoginModule implements LoginModule { 070 private static Log log = LogFactory.getLog(RepeatedFailureLockoutLoginModule.class); 071 072 public static final String FAILURE_COUNT_OPTION = "failureCount"; 073 public static final String FAILURE_PERIOD_OPTION = "failurePeriodSecs"; 074 public static final String LOCKOUT_DURATION_OPTION = "lockoutDurationSecs"; 075 public final static List<String> supportedOptions = Collections.unmodifiableList(Arrays.asList(FAILURE_COUNT_OPTION, FAILURE_PERIOD_OPTION, LOCKOUT_DURATION_OPTION)); 076 077 private static final HashMap<String, LoginHistory> userData = new HashMap<String, LoginHistory>(); 078 private CallbackHandler handler; 079 private String username; 080 private int failureCount = 5; 081 private int failurePeriod = 5 * 60 * 1000; 082 private int lockoutDuration = 30 * 60 * 1000; 083 084 /** 085 * Reads the configuration settings for this module. 086 */ 087 public void initialize(Subject subject, CallbackHandler callbackHandler, 088 Map sharedState, Map options) { 089 for(Object option: options.keySet()) { 090 if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option) 091 && !WrappingLoginModule.supportedOptions.contains(option)) { 092 log.warn("Ignoring option: "+option+". Not supported."); 093 } 094 } 095 String fcString = (String) options.get(FAILURE_COUNT_OPTION); 096 if(fcString != null) { 097 fcString = fcString.trim(); 098 if(!fcString.equals("")) { 099 failureCount = Integer.parseInt(fcString); 100 } 101 } 102 String fpString = (String) options.get(FAILURE_PERIOD_OPTION); 103 if(fpString != null) { 104 fpString = fpString.trim(); 105 if(!fpString.equals("")) { 106 failurePeriod = Integer.parseInt(fpString) * 1000; 107 } 108 } 109 String ldString = (String) options.get(LOCKOUT_DURATION_OPTION); 110 if(ldString != null) { 111 ldString = ldString.trim(); 112 if(!ldString.equals("")) { 113 lockoutDuration = Integer.parseInt(ldString) * 1000; 114 } 115 } 116 handler = callbackHandler; 117 } 118 119 /** 120 * Checks whether the user should be or has been locked out. 121 */ 122 public boolean login() throws LoginException { 123 NameCallback user = new NameCallback("User name:"); 124 Callback[] callbacks = new Callback[]{user}; 125 try { 126 handler.handle(callbacks); 127 } catch (Exception e) { 128 throw (LoginException)new LoginException("Unable to process callback: "+e.getMessage()).initCause(e); 129 } 130 if(callbacks.length != 1) { 131 throw new IllegalStateException("Number of callbacks changed by server!"); 132 } 133 user = (NameCallback) callbacks[0]; 134 username = user.getName(); 135 if(username != null) { 136 LoginHistory history; 137 synchronized (userData) { 138 history = userData.get(username); 139 } 140 if(history != null && !history.isLoginAllowed(lockoutDuration, failurePeriod, failureCount)) { 141 username = null; 142 throw new FailedLoginException("Maximum login failures exceeded; try again later"); 143 } 144 } 145 return false; 146 } 147 148 /** 149 * This module does nothing if a login succeeds. 150 */ 151 public boolean commit() throws LoginException { 152 return false; 153 } 154 155 /** 156 * Notes that (and when) a login failure occured, used to calculate 157 * whether the user should be locked out. 158 */ 159 public boolean abort() throws LoginException { 160 if(username != null) { //work around initial "fake" login 161 LoginHistory history; 162 synchronized (userData) { 163 history = userData.get(username); 164 if(history == null) { 165 history = new LoginHistory(username); 166 userData.put(username, history); 167 } 168 } 169 history.addFailure(); 170 username = null; 171 } 172 return false; 173 } 174 175 /** 176 * This module does nothing on a logout. 177 */ 178 public boolean logout() throws LoginException { 179 username = null; 180 return false; 181 } 182 183 /** 184 * Tracks failure attempts for a user, and calculates lockout 185 * status and expiry, etc. 186 */ 187 private static class LoginHistory implements Serializable { 188 private static final long serialVersionUID = 7792298296084531182L; 189 190 private String user; 191 private LinkedList<Long> data = new LinkedList<Long>(); 192 private long lockExpires = -1; 193 194 public LoginHistory(String user) { 195 this.user = user; 196 } 197 198 public String getUser() { 199 return user; 200 } 201 202 /** 203 * Cleans up the failure history and then calculates whether this user 204 * is locked out or not. 205 */ 206 public synchronized boolean isLoginAllowed(int lockoutLengthMillis, int failureAgeMillis, int maxFailures) { 207 long now = System.currentTimeMillis(); 208 cleanup(now - failureAgeMillis); 209 if(lockExpires > now) { 210 return false; 211 } 212 if(data.size() >= maxFailures) { 213 lockExpires = data.getLast() + lockoutLengthMillis; 214 if(lockExpires > now) { 215 return false; 216 } 217 } 218 return true; 219 } 220 221 /** 222 * Notes that a failure occured. 223 */ 224 public synchronized void addFailure() { 225 data.add(System.currentTimeMillis()); 226 } 227 228 /** 229 * Cleans up all failure records outside the window of time we care 230 * about. 231 */ 232 public synchronized void cleanup(long ignoreOlderThan) { 233 for (Iterator it = data.iterator(); it.hasNext();) { 234 Long time = (Long) it.next(); 235 if(time < ignoreOlderThan) { 236 it.remove(); 237 } else { 238 break; 239 } 240 } 241 } 242 } 243 }