
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.13 Show documentation
Show all versions of play-recaptcha_2.13 Show documentation
Google reCAPTCHA integration for Play Framework
The newest version!
/*
* Copyright 2017 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.data.FormBinding.Implicits.formBinding
import play.api.libs.ws.WSClient
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration.Duration
import java.util.concurrent.TimeUnit
import play.api.libs.json._
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, AmazingDreams
* @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) {
private val logger = Logger(this.getClass)
/**
* Get the request data, regardless of the request format (form, JSON, etc).
*
* Returns an empty map if an unsupported request format.
*
* @param request The implicit request
* @return A map of request parameter values, keyed by parameter name
*/
private def getRequestPostData()(implicit request: play.api.mvc.Request[_]): Map[String, Seq[String]] = {
request.body match {
case body: play.api.mvc.AnyContent if body.asFormUrlEncoded.isDefined =>
body.asFormUrlEncoded.get
case body: play.api.mvc.AnyContent if body.asMultipartFormData.isDefined =>
body.asMultipartFormData.get.asFormUrlEncoded
case body: play.api.mvc.AnyContent if body.asJson.isDefined =>
fromJson(js = body.asJson.get)
case body: Map[_, _] =>
body.asInstanceOf[Map[String, Seq[String]]]
case body: play.api.mvc.MultipartFormData[_] =>
body.asFormUrlEncoded
case body: play.api.libs.json.JsValue =>
fromJson(js = body)
case _ =>
Map.empty[String, Seq[String]]
}
}
/**
* Recursively converts JSON data.
*
* This code was provided by AmazingDreams and comes from the Play source code. It does not support certain
* corner cases like repeating object names or JSON nulls.
*
* @param prefix The current parameter name prefix
* @param js The current JSON object
* @return A map of each request parameter value keyed by parameter name, where sub-objects have names of the form
* "<parent>.<child>" etc (recursing to any number of levels).
*/
private[recaptcha] def fromJson(prefix: String = "", js: JsValue): Map[String, Seq[String]] = js match {
case JsObject(fields) => {
val f: Iterable[Map[String, Seq[String]]] = fields.map {
case (key, value) => {
val recursiveKey = if (prefix == "") key else prefix + "." + key
fromJson(recursiveKey, value)
}
}
f.foldLeft(Map.empty[String, Seq[String]])(_ ++ _)
}
case JsArray(values) => {
values.zipWithIndex.map {
case (value, i) => fromJson(prefix + "[" + i + "]", value)
}.foldLeft(Map.empty[String, Seq[String]])((a, b) => a ++ b)
}
case JsNull => Map.empty[String, Seq[String]]
case JsUndefined() => Map.empty[String, Seq[String]]
case JsBoolean(value) => Map(prefix -> Seq(value.toString))
case JsNumber(value) => Map(prefix -> Seq(value.toString))
case JsString(value) => Map(prefix -> Seq(value.toString))
}
/**
* 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(getRequestPostData())
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"
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy