/* * The MIT License * * Copyright 2013 Tim Boudreau. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.mastfrog.acteur.auth; import com.google.inject.ImplementedBy; import com.mastfrog.acteur.HttpEvent; import com.mastfrog.acteur.Page; import com.mastfrog.acteur.Response; import com.mastfrog.acteur.headers.Headers; import com.mastfrog.acteur.util.BasicCredentials; import com.mastfrog.acteur.util.Realm; import com.mastfrog.settings.Settings; import com.mastfrog.util.perf.Benchmark; import com.mastfrog.util.perf.Benchmark.Kind; import io.netty.channel.ChannelFutureListener; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpResponseStatus.SERVICE_UNAVAILABLE; import java.io.IOException; import java.util.Map; import javax.inject.Inject; import org.joda.time.Duration; import org.joda.time.format.PeriodFormatter; import org.joda.time.format.PeriodFormatterBuilder; /** * Basic authentication Acteur, with a few tricks: * <ul> * <li>Will "tarpit" IP addresses which make repeated failed requests</li> * <li>Will delay sending the response if more than a threshold number of bad * attempts have already been made</li> * </ul> * * @author Tim Boudreau * @deprecated Do not use directly - bind AuthenticationActeur instead, so that * authentication strategies are pluggable */ @Deprecated public class AuthenticateBasicActeur extends AuthenticationActeur { public static final String SETTINGS_KEY_TARPIT_BAD_LOGIN_ATTEMPT_COUNT = "max.allowed.failed.login.attempts"; public static final String SETTINGS_KEY_TARPIT_DELAY_RESPONSE_AFTER = "delay.failed.login.attempts.after"; public static final String SETTINGS_KEY_TARPIT_DELAY_SECONDS = "failed.login.attempt.response.delay"; public static final int DEFAULT_FAILED_LOGIN_ATTEMPT_LIMIT = 7; public static final int DEFAULT_FAILED_LOGIN_ATTEMPT_DELAY_THRESHOLD = 3; public static final int DEFAULT_FAILED_LOGIN_DELAY_SECONDS_MULTIPLIER = 1; @Inject AuthenticateBasicActeur(HttpEvent event, Authenticator authenticator, Realm r, Tarpit tarpit, Settings settings, AuthenticationDecorator decorator, Page page) throws IOException { int badRequestCount = tarpit.count(event); if (badRequestCount > 0 && badRequestCount > settings.getInt(SETTINGS_KEY_TARPIT_BAD_LOGIN_ATTEMPT_COUNT, DEFAULT_FAILED_LOGIN_ATTEMPT_LIMIT)) { setState(new RespondWith(SERVICE_UNAVAILABLE, "Too many bad password attempts")); return; } BasicCredentials credentials = event.getHeader(Headers.AUTHORIZATION); if (credentials == null) { unauthorized(r, event, decorator, page, response()); } else { Object[] stuff = authenticator.authenticate(r.toString(), credentials); if (stuff == null) { int badCount = tarpit.add(event); if (badCount > settings.getInt(SETTINGS_KEY_TARPIT_DELAY_RESPONSE_AFTER, DEFAULT_FAILED_LOGIN_ATTEMPT_DELAY_THRESHOLD)) { delayResponse(badCount, settings); } unauthorized(r, event, decorator, page, response()); } else { decorator.onAuthenticationSucceeded(event, page, response(), stuff); next(stuff); } } } @Benchmark(value = "tarpittedClients", publish = Kind.CALL_COUNT) void delayResponse(int badCount, Settings settings) { Duration delayResponse = Duration.standardSeconds(badCount * settings.getInt(SETTINGS_KEY_TARPIT_DELAY_SECONDS, 1)); response().setDelay(delayResponse); } private static final PeriodFormatter FORMAT = new PeriodFormatterBuilder().appendMinutes() .appendSeparatorIfFieldsBefore(":") .appendSecondsWithMillis().toFormatter(); @Benchmark(value = "failedAuthentication", publish = Kind.CALL_COUNT) private void unauthorized(Realm realm, HttpEvent evt, AuthenticationDecorator decorator, Page page, Response response) { decorator.onAuthenticationFailed(null, page, response); add(Headers.WWW_AUTHENTICATE, realm); setState(new RespondWith(HttpResponseStatus.UNAUTHORIZED)); setResponseBodyWriter(ChannelFutureListener.CLOSE); } @Override public void describeYourself(Map<String, Object> into) { into.put("Basic Authentication Required", true); } /** * Decorator which can do things to the response on authentication * succeess/failure, such as setting/clearing cookies */ @ImplementedBy(NoOpDecorator.class) public interface AuthenticationDecorator { /** * Called when authentication succeeds. This may be called on every * request with basic auth. In particular, if you are going to set a * cookie, ensure it is not already there and valid. * * @param evt The event/request * @param page The page in question * @param response The response * @param stuff Objects returned by Authenticator.authenticate() */ void onAuthenticationSucceeded(HttpEvent evt, Page page, Response response, Object[] stuff); /** * Called when authetication failse * * @param evt The event * @param page The page * @param response The response */ void onAuthenticationFailed(HttpEvent evt, Page page, Response response); } private static class NoOpDecorator implements AuthenticationDecorator { @Override public void onAuthenticationSucceeded(HttpEvent evt, Page page, Response response, Object[] stuff) { //do nothing } @Override public void onAuthenticationFailed(HttpEvent evt, Page page, Response response) { //do nothing } } }