diff --git a/samples/java/Getting Started.md b/samples/java/Getting Started.md index 80199cac..32da8998 100644 --- a/samples/java/Getting Started.md +++ b/samples/java/Getting Started.md @@ -52,6 +52,24 @@ The configuration file is also used to configure the different authentication pr A list of all configuration options can be found here TODO. +### Configuring Google reCaptcha v2 + +In the play-authenticate-usage sample for Java, Google reCaptcha v2 +is available for the basic authentication signup flow to protect +again signup spam bots. + +To enable the Google reCaptcha, you need to signup for a site key and secret key here: https://www.google.com/recaptcha/admin#createsite + +Place the secret key and site key in your mine.conf file: + +gcaptcha { + gsiteKey="6LdA6QATAAAAAIPjXXXXXXXXXXXyBbuiFIBu75j8PU" + gsecretKey="6LdA6QATAAAAAI555555555555555L96b4rrNkDy" + } + +The reCaptcha is displayed in the signup.scala.html view. +The code that manages the reCaptcha is in the Application.java controller. + ### Configuring Mail You also have to configure smtp and play-easymail. For testing purposes you can add the following line to your application.conf file: diff --git a/samples/java/play-authenticate-usage/app/controllers/Application.java b/samples/java/play-authenticate-usage/app/controllers/Application.java index 32868aac..f9f9eeff 100644 --- a/samples/java/play-authenticate-usage/app/controllers/Application.java +++ b/samples/java/play-authenticate-usage/app/controllers/Application.java @@ -2,6 +2,20 @@ import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Map; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; + +import play.libs.ws.*; +import play.libs.F.Function; +import play.libs.F.Promise; +import play.libs.Json; import models.User; import play.Routes; @@ -14,87 +28,178 @@ import providers.MyUsernamePasswordAuthProvider.MyLogin; import providers.MyUsernamePasswordAuthProvider.MySignup; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValue; + import views.html.*; import be.objectify.deadbolt.java.actions.Group; import be.objectify.deadbolt.java.actions.Restrict; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import com.feth.play.module.pa.PlayAuthenticate; import com.feth.play.module.pa.providers.password.UsernamePasswordAuthProvider; import com.feth.play.module.pa.user.AuthUser; public class Application extends Controller { - public static final String FLASH_MESSAGE_KEY = "message"; - public static final String FLASH_ERROR_KEY = "error"; - public static final String USER_ROLE = "user"; - - public static Result index() { - return ok(index.render()); - } - - public static User getLocalUser(final Session session) { - final AuthUser currentAuthUser = PlayAuthenticate.getUser(session); - final User localUser = User.findByAuthUserIdentity(currentAuthUser); - return localUser; - } - - @Restrict(@Group(Application.USER_ROLE)) - public static Result restricted() { - final User localUser = getLocalUser(session()); - return ok(restricted.render(localUser)); - } - - @Restrict(@Group(Application.USER_ROLE)) - public static Result profile() { - final User localUser = getLocalUser(session()); - return ok(profile.render(localUser)); - } - - public static Result login() { - return ok(login.render(MyUsernamePasswordAuthProvider.LOGIN_FORM)); - } - - public static Result doLogin() { - com.feth.play.module.pa.controllers.Authenticate.noCache(response()); - final Form filledForm = MyUsernamePasswordAuthProvider.LOGIN_FORM - .bindFromRequest(); - if (filledForm.hasErrors()) { - // User did not fill everything properly - return badRequest(login.render(filledForm)); - } else { - // Everything was filled - return UsernamePasswordAuthProvider.handleLogin(ctx()); - } - } - - public static Result signup() { - return ok(signup.render(MyUsernamePasswordAuthProvider.SIGNUP_FORM)); - } - - public static Result jsRoutes() { - return ok( - Routes.javascriptRouter("jsRoutes", - controllers.routes.javascript.Signup.forgotPassword())) - .as("text/javascript"); - } - - public static Result doSignup() { - com.feth.play.module.pa.controllers.Authenticate.noCache(response()); - final Form filledForm = MyUsernamePasswordAuthProvider.SIGNUP_FORM - .bindFromRequest(); - if (filledForm.hasErrors()) { - // User did not fill everything properly - return badRequest(signup.render(filledForm)); - } else { - // Everything was filled - // do something with your part of the form before handling the user - // signup - return UsernamePasswordAuthProvider.handleSignup(ctx()); - } - } - - public static String formatTimestamp(final long t) { - return new SimpleDateFormat("yyyy-dd-MM HH:mm:ss").format(new Date(t)); - } + public static final String FLASH_MESSAGE_KEY = "message"; + public static final String FLASH_ERROR_KEY = "error"; + public static final String USER_ROLE = "user"; + + public static Result index() { + return ok(index.render()); + } + + public static User getLocalUser(final Session session) { + final AuthUser currentAuthUser = PlayAuthenticate.getUser(session); + final User localUser = User.findByAuthUserIdentity(currentAuthUser); + return localUser; + } + + @Restrict(@Group(Application.USER_ROLE)) + public static Result restricted() { + final User localUser = getLocalUser(session()); + return ok(restricted.render(localUser)); + } + + @Restrict(@Group(Application.USER_ROLE)) + public static Result profile() { + final User localUser = getLocalUser(session()); + return ok(profile.render(localUser)); + } + + public static Result login() { + return ok(login.render(MyUsernamePasswordAuthProvider.LOGIN_FORM)); + } + + public static Result doLogin() { + com.feth.play.module.pa.controllers.Authenticate.noCache(response()); + final Form filledForm = MyUsernamePasswordAuthProvider.LOGIN_FORM + .bindFromRequest(); + if (filledForm.hasErrors()) { + // User did not fill everything properly + return badRequest(login.render(filledForm)); + } else { + // Everything was filled + return UsernamePasswordAuthProvider.handleLogin(ctx()); + } + } + + public static Result signup() { + return ok(signup.render(MyUsernamePasswordAuthProvider.SIGNUP_FORM)); + } + + public static Result jsRoutes() { + return ok( + Routes.javascriptRouter("jsRoutes", + controllers.routes.javascript.Signup.forgotPassword())) + .as("text/javascript"); + } + + public static String captchaResp(String gcaptchaCode) { + String googUrl = "https://www.google.com/recaptcha/api/siteverify"; + String encSecret = ""; + String encCapcode = ""; + String error = "-1"; + URL url = null; + // Get the secret key + Config conf = ConfigFactory.load(); + String gsecretKey = conf.getString("play-authenticate.gcaptcha.gsecretKey"); + // Debug -- show values on console + // System.out.println("gsecretKey = " + gsecretKey); + // System.out.println("captchacode = " + gcaptchaCode); + try { + encSecret = URLEncoder.encode(gsecretKey, "UTF-8"); + encCapcode = URLEncoder.encode(gcaptchaCode, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return error; + } + String query = "secret=" + encSecret + "&response=" + encCapcode; + try { + url = new URL(googUrl + "?" + query); + } catch (MalformedURLException e) { + e.printStackTrace(); + return error; + } + StringBuilder stringBuilder = new StringBuilder(); + try { + // Check if Google validates the captcha response + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "application/json"); + // 10 seconds max to respond + connection.setReadTimeout(10 * 1000); + connection.connect(); + if (connection.getResponseCode() != 200) { + throw new RuntimeException("Failed : HTTP error code : " + + connection.getResponseCode()); + } + // read the output + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + return stringBuilder.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return error; + } + + public static Result doSignup() { + com.feth.play.module.pa.controllers.Authenticate.noCache(response()); + final Form filledForm = MyUsernamePasswordAuthProvider.SIGNUP_FORM + .bindFromRequest(); + + final Map values = request().body().asFormUrlEncoded(); + final String gcaptchaCode = values.get("g-recaptcha-response")[0]; + String error = "-1"; + + if (filledForm.hasErrors()) { + // User did not fill everything properly + return badRequest(signup.render(filledForm)); + } else { + // Everything was filled + // do something with your part of the form before handling the user + // signup + // + // Check if captcha was filled in + if (gcaptchaCode == null || gcaptchaCode.isEmpty()) { + flash("error", "You need to successfully solve the reCAPTCHA at the bottom of the form in order to signup."); + return badRequest(signup.render(filledForm)); + } + + // Find out if Google likes the Captcha + String json = captchaResp(gcaptchaCode); + + // Check if an error occured while contacting Google and processing + if (json.equals(error)) { + flash("error", "An error occured while attempting to resolve the Google Captcha. Try again?"); + return badRequest(signup.render(filledForm)); + } + + // Turn the json string into a Json object + JsonNode jobj = Json.parse(json); + Boolean captchaPassed = jobj.findPath("success").booleanValue(); + + if (captchaPassed) { + return UsernamePasswordAuthProvider.handleSignup(ctx()); + } else { + // Error codes are in jobj.findPath("error-codes").textValue(); + flash("error", "You need to successfully solve the reCAPTCHA at the bottom of the form in order to signup."); + return badRequest(signup.render(filledForm)); + } + } + } + + public static String formatTimestamp(final long t) { + return new SimpleDateFormat("yyyy-dd-MM HH:mm:ss").format(new Date(t)); + } } \ No newline at end of file diff --git a/samples/java/play-authenticate-usage/app/models/User.java b/samples/java/play-authenticate-usage/app/models/User.java index 58565eb8..5d5cd854 100644 --- a/samples/java/play-authenticate-usage/app/models/User.java +++ b/samples/java/play-authenticate-usage/app/models/User.java @@ -45,7 +45,7 @@ public class User extends Model implements Subject { public String firstName; public String lastName; - + @Formats.DateTime(pattern = "yyyy-MM-dd HH:mm:ss") public Date lastLogin; diff --git a/samples/java/play-authenticate-usage/app/views/main.scala.html b/samples/java/play-authenticate-usage/app/views/main.scala.html index 26567712..273f554f 100644 --- a/samples/java/play-authenticate-usage/app/views/main.scala.html +++ b/samples/java/play-authenticate-usage/app/views/main.scala.html @@ -16,6 +16,7 @@ + diff --git a/samples/java/play-authenticate-usage/app/views/signup.scala.html b/samples/java/play-authenticate-usage/app/views/signup.scala.html index 96c7b9a0..ecc145c1 100644 --- a/samples/java/play-authenticate-usage/app/views/signup.scala.html +++ b/samples/java/play-authenticate-usage/app/views/signup.scala.html @@ -31,6 +31,10 @@

@Messages("playauthenticate.signup.title")

@_emailPartial(signupForm) @_passwordPartial(signupForm) + +
Click in the square next to "I'm not a robot"
+
+
} diff --git a/samples/java/play-authenticate-usage/conf/play-authenticate/mine.conf b/samples/java/play-authenticate-usage/conf/play-authenticate/mine.conf index 638f6c7a..fb1cbb98 100644 --- a/samples/java/play-authenticate-usage/conf/play-authenticate/mine.conf +++ b/samples/java/play-authenticate-usage/conf/play-authenticate/mine.conf @@ -33,6 +33,14 @@ play-authenticate { loginAfterPasswordReset=true } + # Settings for Google CAPTCHA + # if you are not using it, then you can remove this portion + # of the config file + gcaptcha { + gsiteKey="6LdA6QATAAAAAIPjXXXXXXXXXXXyBbuiFIBu75j8PU" + gsecretKey="6LdA6QATAAAAAI555555555555555L96b4rrNkDy" + } + # Settings for the http basic auth provider # if you are not using it (and you shouldn't), you can remove this portion # of the config file