/* * * * Copyright (c) 2016. David Sowerby * * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * * the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * * specific language governing permissions and limitations under the License. * */ package uk.q3c.krail.core.shiro; import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.UsernamePasswordToken; import javax.annotation.concurrent.ThreadSafe; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; @Singleton @ThreadSafe public class DefaultLoginAttemptLog implements LoginAttemptLog { public enum LogOutcome { PASS, FAIL, RESET } private final Map<String, List<LogEntry>> history = new TreeMap<>(); private final Map<String, Integer> unsuccessfulAttempts = new TreeMap<>(); private final Map<String, LocalDateTime> lastSuccessful = new TreeMap<>(); private int maxAttempts = 3; @Override public synchronized void setMaximumAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; } @Override public synchronized void recordSuccessfulAttempt(UsernamePasswordToken upToken) { LogEntry log = createLog(upToken, LogOutcome.PASS); unsuccessfulAttempts.remove(upToken.getUsername()); lastSuccessful.put(upToken.getUsername(), log.dateTime); } private LogEntry createLog(UsernamePasswordToken upToken, LogOutcome logOutcome) { String username = upToken.getUsername(); return createLog(username, logOutcome); } private LogEntry createLog(String username, LogOutcome logOutcome) { LogEntry logEntry = new LogEntry(logOutcome); List<LogEntry> list = history.get(username); if (list == null) { list = new ArrayList<>(); history.put(username, list); } list.add(logEntry); return logEntry; } /** * records a failed login attempt and throws a ExcessiveAttemptsException if the number of attempts exceeds * {@link #maxAttempts} * * @see uk.q3c.krail.core.shiro.LoginAttemptLog#recordFailedAttempt(org.apache.shiro.authc.UsernamePasswordToken) */ @Override public synchronized void recordFailedAttempt(UsernamePasswordToken upToken) { createLog(upToken, LogOutcome.FAIL); Integer failedAttempts = unsuccessfulAttempts.get(upToken.getUsername()); if (failedAttempts == null) { failedAttempts = 0; } unsuccessfulAttempts.put(upToken.getUsername(), Integer.valueOf(failedAttempts + 1)); int attemptsLeft = attemptsRemaining(upToken.getUsername()); if (attemptsLeft == 0) { throw new ExcessiveAttemptsException("Login failed after maximum attempts"); } } @Override public synchronized int attemptsRemaining(String username) { Integer attemptsMade = unsuccessfulAttempts.get(username); // no unsuccessful attempt has been made to login, there won't be an entry if (attemptsMade == null) { attemptsMade = 0; } return maxAttempts - attemptsMade; } @Override public synchronized void clearHistory(String username) { history.remove(username); } @Override public synchronized void resetAttemptCount(String username) { unsuccessfulAttempts.remove(username); createLog(username, LogOutcome.RESET); } @Override public synchronized void clearHistory() { history.clear(); } @Override public synchronized void resetAttemptCount() { unsuccessfulAttempts.clear(); } @Override public synchronized LocalDateTime dateOfLastSuccess(String username) { return lastSuccessful.get(username); } @Override public synchronized LogEntry latestLog(String username) { List<LogEntry> list = history.get(username); return list.get(list.size() - 1); } @Override public synchronized ImmutableList<LogEntry> historyFor(String username) { List<LogEntry> list = history.get(username); if (list == null) { list = new ArrayList<>(); } return ImmutableList.copyOf(list); } public static class LogEntry { private final LocalDateTime dateTime; private final LogOutcome logOutcome; public LogEntry(LogOutcome logOutcome) { this.logOutcome = logOutcome; dateTime = LocalDateTime.now(); } public LocalDateTime getDateTime() { return dateTime; } public LogOutcome getLogOutcome() { return logOutcome; } } }