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 }