
com.nappin.play.recaptcha.RecaptchaVerifier.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of play-recaptcha_2.11 Show documentation
Show all versions of play-recaptcha_2.11 Show documentation
Google reCAPTCHA integration for Play Framework
/*
* Copyright 2016 Chris Nappin
*
* 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 com.nappin.play.recaptcha
import javax.inject.{Inject, Singleton}
import play.api.Logger
import play.api.mvc.{AnyContent, Request}
import play.api.data.Form
import play.api.libs.ws.WSClient
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration.Duration
import java.util.concurrent.TimeUnit
object RecaptchaVerifier {
/** The artificial form field key used for captcha errors. */
val formErrorKey = "com.nappin.play.recaptcha.error"
/** The recaptcha challenge field name. */
val recaptchaChallengeField = "recaptcha_challenge_field"
/** The recaptcha (v2) response field name. */
val recaptchaV2ResponseField = "g-recaptcha-response"
}
/**
* Verifies whether a recaptcha response is valid, by invoking the Google Recaptcha verify web
* service.
*
* @author chrisnappin
* @constructor Creates a new instance.
* @param settings The Recaptcha settings
* @param parser The response parser to use
* @param wsClient The web service client to use
*/
@Singleton
class RecaptchaVerifier @Inject() (settings: RecaptchaSettings, parser: ResponseParser, wsClient: WSClient) {
val logger = Logger(this.getClass)
/**
* High level API (using Play forms).
*
* Binds form data from the request, and if valid then verifies the recaptcha response, by
* invoking the Google Recaptcha verify web service (API v2) in a reactive manner.
* Possible errors include:
*
* - Binding error (form validation, regardless of recaptcha)
* - Recaptcha response missing (end user didn't enter it)
* - Recpatcha response incorrect
* - Error invoking the recaptcha service
*
*
* Apart from generic binding error, the recaptcha errors are populated against the artificial
* form field key formErrorKey
, handled by the recaptcha view template tags.
*
* @param form The form
* @param request Implicit - The current web request
* @param context Implicit - The execution context used for futures
* @return A future that will be the form to use, either populated with an error or success
* @throws IllegalStateException Developer errors that shouldn't happen - no recaptcha
* challenge, or multiple challenges or responses found
*/
def bindFromRequestAndVerify[T](form: Form[T])(implicit request: Request[AnyContent],
context: ExecutionContext): Future[Form[T]] = {
val boundForm = form.bindFromRequest
val response = readResponse(request.body.asFormUrlEncoded.get)
if (response.length < 1) {
// probably an end user error
logger.debug("User did not enter a captcha response in the form POST submitted")
// return the missing required field, plus any other form bind errors that might have happened
return Future { boundForm.withError(
RecaptchaVerifier.formErrorKey, RecaptchaErrorCode.responseMissing) }
}
boundForm.fold(
// form binding failed, so don't call recaptcha
error => Future {
logger.debug("Binding error")
error
},
// form binding succeeded, so verify the captcha response
success => {
val result = verifyV2(response, request.remoteAddress)
result.map { r => {
r.fold(
// captcha incorrect, or a technical error
error => boundForm.withError(RecaptchaVerifier.formErrorKey, error.code),
// all success
success => boundForm
)
}
}
})
}
/**
* Read the response field from the POST'ed response.
*
* @param params The POST parameters
* @return The response
* @throws IllegalStateException If response missing or multiple
*/
private def readResponse(params: Map[String, Seq[String]]): String = {
val fieldName = RecaptchaVerifier.recaptchaV2ResponseField
if (!params.contains(fieldName)) {
// probably a developer error
val message = "No recaptcha response POSTed, check the form submitted was valid"
logger.error(message)
throw new IllegalStateException(message)
} else if (params(fieldName).size > 1) {
// probably a developer error
val message = "Multiple recaptcha responses POSTed, check the form submitted was valid"
logger.error(message)
throw new IllegalStateException(message)
}
params(fieldName).head
}
/**
* Low level API (independent of Play form and request APIs).
*
* Verifies whether a recaptcha response is valid, by invoking the Google Recaptcha API version
* 2 verify web service in a reactive manner.
*
* @param response The recaptcha response, to verify
* @param remoteIp The IP address of the end user
* @param context Implicit - The execution context used for futures
* @return A future that will be either an Error (with a code) or Success
*/
def verifyV2(response: String, remoteIp: String)(
implicit context: ExecutionContext): Future[Either[Error, Success]] = {
// create the v2 POST payload
val payload = Map(
"secret" -> Seq(settings.privateKey),
"response" -> Seq(response),
"remoteip" -> Seq(remoteIp)
)
logger.info(s"Verifying v2 recaptcha ($response) for $remoteIp")
val futureResponse = wsClient.url(settings.verifyUrl)
.withRequestTimeout(Duration(settings.requestTimeoutMs, TimeUnit.MILLISECONDS))
.post(payload)
futureResponse.map { response => {
if (response.status == play.api.http.Status.OK) {
parser.parseV2Response(response.json)
} else {
logger.error("Error calling recaptcha v2 API, HTTP response " + response.status)
Left(Error(RecaptchaErrorCode.recaptchaNotReachable))
}
}
} recover {
case ex: java.io.IOException =>
logger.error("Unable to call recaptcha v2 API" , ex)
Left(Error(RecaptchaErrorCode.recaptchaNotReachable))
// e.g. various JSON parsing errors are possible
case other: Any =>
logger.error("Error calling recaptcha v2 API: " + other.getMessage)
Left(Error(RecaptchaErrorCode.apiError))
}
}
}
/**
* Used to hold various recaptcha error codes. Some are defined by Google Recaptcha, some are used
* solely by this module. Yes, the Google Recpatcha documentation states not to rely on these, so
* we deliberately keep these to a minimum.
*/
object RecaptchaErrorCode {
/** Defined in Google Recaptcha documentation, returned by Recaptcha itself, means the captcha was incorrect. */
val captchaIncorrect = "incorrect-captcha-sol"
/** Defined in Google Recaptcha documentation, used by this module, means recaptcha itself couldn't be reached. */
val recaptchaNotReachable = "recaptcha-not-reachable"
/** An API error (e.g. invalid format response). */
val apiError = "recaptcha-api-error"
/** The recaptcha response was missing from the request (probably an end user error). */
val responseMissing = "recaptcha-response-missing"
/** Error codes that are only for internal use by this module, and shouldn't be passed to the recaptcha API. */
val internalErrorCodes = Seq(recaptchaNotReachable, apiError, responseMissing)
/**
* Determine whether the specified error code is for internal use only by this module, and shouldn't be passed
* to the recaptcha API.
* @param errorCode The error code
* @return true
if internal
*/
def isInternalErrorCode(errorCode: String): Boolean = internalErrorCodes.contains(errorCode)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy