/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.util; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.option.ApplicationPage; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmUnrecoverableException; import password.pwm.http.HttpMethod; import password.pwm.http.PwmRequest; import password.pwm.http.PwmRequestAttribute; import password.pwm.http.PwmURL; import password.pwm.http.client.PwmHttpClient; import password.pwm.http.client.PwmHttpClientRequest; import password.pwm.http.client.PwmHttpClientResponse; import password.pwm.svc.PwmService; import password.pwm.svc.intruder.IntruderManager; import password.pwm.svc.stats.Statistic; import password.pwm.svc.stats.StatisticsManager; import password.pwm.util.java.JsonUtil; import password.pwm.util.logging.PwmLogger; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; public class CaptchaUtility { private static final PwmLogger LOGGER = PwmLogger.getLogger(CaptchaUtility.class.getName()); private static final String COOKIE_SKIP_INSTANCE_VALUE = "INSTANCEID"; /** * Verify a reCaptcha request. The reCaptcha request API is documented at <a href="http://recaptcha.net/apidocs/captcha/">reCaptcha API. */ public static boolean verifyReCaptcha( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { final String recaptchaResponse = pwmRequest.readParameterAsString("g-recaptcha-response"); return verifyReCaptcha(pwmRequest, recaptchaResponse); } public static boolean verifyReCaptcha( final PwmRequest pwmRequest, final String recaptchaResponse ) throws PwmUnrecoverableException { if (!captchaEnabledForRequest(pwmRequest)) { return true; } final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PasswordData privateKey = pwmApplication.getConfig().readSettingAsPassword(PwmSetting.RECAPTCHA_KEY_PRIVATE); final StringBuilder bodyText = new StringBuilder(); bodyText.append("secret=").append(privateKey.getStringValue()); bodyText.append("&"); bodyText.append("remoteip=").append(pwmRequest.getSessionLabel().getSrcAddress()); bodyText.append("&"); bodyText.append("response=").append(recaptchaResponse); try { final PwmHttpClientRequest clientRequest = new PwmHttpClientRequest( HttpMethod.POST, pwmApplication.getConfig().readAppProperty(AppProperty.RECAPTCHA_VALIDATE_URL), bodyText.toString(), Collections.singletonMap("Content-Type",PwmConstants.ContentTypeValue.form.getHeaderValue()) ); LOGGER.debug(pwmRequest, "sending reCaptcha verification request" ); final PwmHttpClient client = new PwmHttpClient(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel()); final PwmHttpClientResponse clientResponse = client.makeRequest(clientRequest); if (clientResponse.getStatusCode() != HttpServletResponse.SC_OK) { throw new PwmUnrecoverableException(new ErrorInformation( PwmError.ERROR_CAPTCHA_API_ERROR, "unexpected HTTP status code (" + clientResponse.getStatusCode() + ")" )); } final JsonElement responseJson = new JsonParser().parse(clientResponse.getBody()); final JsonObject topObject = responseJson.getAsJsonObject(); if (topObject != null && topObject.has("success")) { final boolean success = topObject.get("success").getAsBoolean(); if (success) { return true; } if (topObject.has("error-codes")) { final List<String> errorCodes = new ArrayList<>(); for (final JsonElement element : topObject.get("error-codes").getAsJsonArray()) { final String errorCode = element.getAsString(); errorCodes.add(errorCode); } LOGGER.debug(pwmRequest, "recaptcha error codes: " + JsonUtil.serializeCollection(errorCodes)); } } writeCaptchaSkipCookie(pwmRequest); } catch (Exception e) { final String errorMsg = "unexpected error during reCaptcha API execution: " + e.getMessage(); LOGGER.error(errorMsg,e); final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_CAPTCHA_API_ERROR, errorMsg); final PwmUnrecoverableException pwmE = new PwmUnrecoverableException(errorInfo); pwmE.initCause(e); throw pwmE; } return false; } private static void writeCaptchaSkipCookie( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { final String cookieValue = figureSkipCookieValue(pwmRequest); final int captchaSkipCookieLifetimeSeconds = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_COOKIE_CAPTCHA_SKIP_AGE)); final String captchaSkipCookieName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_COOKIE_CAPTCHA_SKIP_NAME); if (cookieValue != null) { pwmRequest.getPwmResponse().writeCookie( captchaSkipCookieName, cookieValue, captchaSkipCookieLifetimeSeconds ); } } private static String figureSkipCookieValue(final PwmRequest pwmRequest) throws PwmUnrecoverableException { String cookieValue = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAPTCHA_SKIP_COOKIE); if (cookieValue == null || cookieValue.trim().length() < 1) { return null; } if (cookieValue.equals(COOKIE_SKIP_INSTANCE_VALUE)) { cookieValue = pwmRequest.getPwmApplication().getInstanceID(); } return cookieValue != null && cookieValue.trim().length() > 0 ? cookieValue : null; } private static boolean checkRequestForCaptchaSkipCookie(final PwmRequest pwmRequest) throws PwmUnrecoverableException { final String allowedSkipValue = figureSkipCookieValue(pwmRequest); final String captchaSkipCookieName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_COOKIE_CAPTCHA_SKIP_NAME); if (allowedSkipValue != null) { final String cookieValue = pwmRequest.readCookie(captchaSkipCookieName); if (allowedSkipValue.equals(cookieValue)) { LOGGER.debug(pwmRequest, "browser has a valid " + captchaSkipCookieName+ " cookie value of " + figureSkipCookieValue(pwmRequest) + ", skipping captcha check"); return true; } } return false; } public static boolean captchaEnabledForRequest( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { if (!checkIfCaptchaConfigEnabled(pwmRequest)) { return false; } if (checkIfCaptchaParamPresent(pwmRequest)) { return false; } if (checkRequestForCaptchaSkipCookie(pwmRequest)) { return false; } if (!checkIntruderCount(pwmRequest)) { return false; } final Set<ApplicationPage> protectedModules = pwmRequest.getConfig().readSettingAsOptionList( PwmSetting.CAPTCHA_PROTECTED_PAGES, ApplicationPage.class ); final PwmURL pwmURL = pwmRequest.getURL(); boolean enabled = false; if (protectedModules != null) { if (protectedModules.contains(ApplicationPage.LOGIN) && pwmURL.isLoginServlet()) { enabled = true; } else if (protectedModules.contains(ApplicationPage.FORGOTTEN_PASSWORD) && pwmURL.isForgottenPasswordServlet()) { enabled = true; } else if (protectedModules.contains(ApplicationPage.FORGOTTEN_USERNAME) && pwmURL.isForgottenUsernameServlet()) { enabled = true; } else if (protectedModules.contains(ApplicationPage.USER_ACTIVATION) && pwmURL.isUserActivationServlet()) { enabled = true; } else if (protectedModules.contains(ApplicationPage.NEW_USER_REGISTRATION) && pwmURL.isNewUserRegistrationServlet()) { enabled = true; } } return enabled; } public static boolean checkIfCaptchaConfigEnabled( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { final Configuration config = pwmRequest.getPwmApplication().getConfig(); final PasswordData privateKey = config.readSettingAsPassword(PwmSetting.RECAPTCHA_KEY_PRIVATE); final String publicKey = config.readSettingAsString(PwmSetting.RECAPTCHA_KEY_PUBLIC); return (privateKey != null && publicKey != null && !publicKey.isEmpty()); } public static void prepareCaptchaDisplay(final PwmRequest pwmRequest) throws ServletException, PwmUnrecoverableException, IOException { StatisticsManager.incrementStat(pwmRequest, Statistic.CAPTCHA_PRESENTATIONS); final String reCaptchaPublicKey = pwmRequest.getConfig().readSettingAsString(PwmSetting.RECAPTCHA_KEY_PUBLIC); pwmRequest.setAttribute(PwmRequestAttribute.CaptchaPublicKey, reCaptchaPublicKey); { final String urlValue = pwmRequest.getConfig().readAppProperty(AppProperty.RECAPTCHA_CLIENT_JS_URL); pwmRequest.setAttribute(PwmRequestAttribute.CaptchaClientUrl, urlValue); } { final String configuredUrl =pwmRequest.getConfig().readAppProperty(AppProperty.RECAPTCHA_CLIENT_IFRAME_URL); final String url = configuredUrl + "?k=" + reCaptchaPublicKey + "&hl=" + pwmRequest.getLocale().toString(); pwmRequest.setAttribute(PwmRequestAttribute.CaptchaIframeUrl,url); } } private static boolean checkIfCaptchaParamPresent(final PwmRequest pwmRequest) throws PwmUnrecoverableException { final String skipCaptcha = pwmRequest.readParameterAsString(PwmConstants.PARAM_SKIP_CAPTCHA); if (skipCaptcha != null && skipCaptcha.length() > 0) { final String configValue = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAPTCHA_SKIP_PARAM); if (configValue != null && configValue.equals(skipCaptcha)) { LOGGER.trace(pwmRequest, "valid skipCaptcha value in request, skipping captcha check for this session"); return true; } else { LOGGER.error(pwmRequest, "skipCaptcha value is in request, however value '" + skipCaptcha + "' does not match configured value"); } } return false; } private static boolean checkIntruderCount(final PwmRequest pwmRequest) { final long maxIntruderCount = pwmRequest.getConfig().readSettingAsLong(PwmSetting.CAPTCHA_INTRUDER_COUNT_TRIGGER); if (maxIntruderCount == 0) { return true; } final int currentSessionAttempts = pwmRequest.getPwmSession().getSessionStateBean().getIntruderAttempts(); if (currentSessionAttempts >= maxIntruderCount) { LOGGER.debug(pwmRequest, "session intruder attempt count '" + currentSessionAttempts + "', therefore captcha will be required"); return true; } final IntruderManager intruderManager = pwmRequest.getPwmApplication().getIntruderManager(); if (intruderManager == null || intruderManager.status() != PwmService.STATUS.OPEN) { return false; } final int intruderAttemptCount = intruderManager.countForNetworkEndpointInRequest(pwmRequest); if (intruderAttemptCount >= maxIntruderCount) { LOGGER.debug(pwmRequest, "network intruder attempt count '" + intruderAttemptCount + "', therefore captcha will be required"); return true; } return false; } }