package org.apereo.cas.web.support;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.inspektr.audit.AuditActionContext;
import org.apereo.inspektr.audit.AuditPointRuntimeInfo;
import org.apereo.inspektr.audit.AuditTrailManager;
import org.apereo.inspektr.common.web.ClientInfo;
import org.apereo.inspektr.common.web.ClientInfoHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
/**
* Works in conjunction with the Inspektr Library to block attempts to dictionary attack users.
* <p>
* Defines a new Inspektr Action "THROTTLED_LOGIN_ATTEMPT" which keeps track of failed login attempts that don't result
* in AUTHENTICATION_FAILED methods
* <p>
* This relies on the default Inspektr table layout and username construction. The username construction can be overridden
* in a subclass.
*
* @author Scott Battaglia
* @since 3.3.5
*/
public class InspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter extends AbstractThrottledSubmissionHandlerInterceptorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(InspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter.class);
private static final double NUMBER_OF_MILLISECONDS_IN_SECOND = 1000.0;
private static final String INSPEKTR_ACTION = "THROTTLED_LOGIN_ATTEMPT";
private final AuditTrailManager auditTrailManager;
private final DataSource dataSource;
private final String applicationCode;
private final String authenticationFailureCode;
private final String sqlQueryAudit;
private JdbcTemplate jdbcTemplate;
/**
* Instantiates a new inspektr throttled submission by ip address and username handler interceptor adapter.
*
* @param failureThreshold the failure threshold
* @param failureRangeInSeconds the failure range in seconds
* @param usernameParameter the username parameter
* @param auditTrailManager the audit trail manager
* @param dataSource the data source
* @param appCode the app code
* @param sqlQueryAudit the sql query audit
* @param authenticationFailureCode the authentication failure code
*/
public InspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter(final int failureThreshold, final int failureRangeInSeconds,
final String usernameParameter,
final AuditTrailManager auditTrailManager,
final DataSource dataSource, final String appCode,
final String sqlQueryAudit, final String authenticationFailureCode) {
super(failureThreshold, failureRangeInSeconds, usernameParameter);
this.auditTrailManager = auditTrailManager;
this.dataSource = dataSource;
this.applicationCode = appCode;
this.sqlQueryAudit = sqlQueryAudit;
this.authenticationFailureCode = authenticationFailureCode;
if (this.dataSource != null) {
this.jdbcTemplate = new JdbcTemplate(this.dataSource);
} else {
LOGGER.warn("No data source is defined for [{}]. Ignoring the construction of JDBC template", this.getName());
}
}
@Override
public boolean exceedsThreshold(final HttpServletRequest request) {
if (this.dataSource != null && this.jdbcTemplate != null) {
final String userToUse = constructUsername(request, getUsernameParameter());
final ZonedDateTime cutoff = ZonedDateTime.now(ZoneOffset.UTC).minusSeconds(getFailureRangeInSeconds());
final ClientInfo clientInfo = ClientInfoHolder.getClientInfo();
final String remoteAddress = clientInfo.getClientIpAddress();
final List<Timestamp> failures = this.jdbcTemplate.query(
this.sqlQueryAudit,
new Object[]{remoteAddress, userToUse, this.authenticationFailureCode,
this.applicationCode, DateTimeUtils.timestampOf(cutoff)},
new int[]{Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP},
(resultSet, i) -> resultSet.getTimestamp(1));
if (failures.size() < 2) {
return false;
}
// Compute rate in submissions/sec between last two authn failures and compare with threshold
return NUMBER_OF_MILLISECONDS_IN_SECOND / (failures.get(0).getTime() - failures.get(1).getTime()) > getThresholdRate();
}
LOGGER.warn("No data source is defined for [{}]. Ignoring threshold checking", this.getName());
return false;
}
@Override
public void recordSubmissionFailure(final HttpServletRequest request) {
recordThrottle(request);
}
@Override
protected void recordThrottle(final HttpServletRequest request) {
if (this.dataSource != null && this.jdbcTemplate != null) {
super.recordThrottle(request);
final String userToUse = constructUsername(request, getUsernameParameter());
final ClientInfo clientInfo = ClientInfoHolder.getClientInfo();
final AuditPointRuntimeInfo auditPointRuntimeInfo = new AuditPointRuntimeInfo() {
private static final long serialVersionUID = 1L;
@Override
public String asString() {
return String.format("%s.recordThrottle()", this.getClass().getName());
}
};
final AuditActionContext context = new AuditActionContext(
userToUse,
userToUse,
INSPEKTR_ACTION,
this.applicationCode,
DateTimeUtils.dateOf(ZonedDateTime.now(ZoneOffset.UTC)),
clientInfo.getClientIpAddress(),
clientInfo.getServerIpAddress(),
auditPointRuntimeInfo);
this.auditTrailManager.record(context);
} else {
LOGGER.warn("No data source is defined for [{}]. Ignoring audit record-keeping", this.getName());
}
}
/**
* Construct username from the request.
*
* @param request the request
* @param usernameParameter the username parameter
* @return the string
*/
private static String constructUsername(final HttpServletRequest request, final String usernameParameter) {
return request.getParameter(usernameParameter);
}
@Override
public String getName() {
return "inspektrIpAddressUsernameThrottle";
}
}