Showing posts with label Web Development. Show all posts
Showing posts with label Web Development. Show all posts

Thursday, 9 June 2022

Integrating Google Cloud reCAPTCHA Enterprise in PHP

Google Cloud reCAPTCHA Enterprise is the successor (?) to reCAPTCHA v3. 

The code snippets Google supplies for integrating reCATPCHA Enterprise in PHP are frustratingly uncohesive and neither a working demo or complete code sample exists (as far as I can find at the time of writing). 

Moreover, the code snippets that are provided were incomplete for my implementation and the number of moving parts (Cloud projects, keys and credentials) is nothing short of overwhelming. 

Given the above, I struggled for some time just get everything working--which is where I'm up to now. So a disclaimer for those of you readers who are proficient PHP developers and who possess a deeper understanding of the Google Cloud Platform, please use this as a starting point only. There may be a better (more refined) approach. 

In terms of the official documentation and the code snippets I refer to above and will discuss in this post, start here: https://cloud.google.com/recaptcha-enterprise/docs/choose-key-type

I should also note I'm working in a basic web hosting environment with PHP installed. It's not a Google environment--think more budget web host ;)

Setup

Before you can get started, you'll need to login to the Google Cloud Platform, create a new project if you don't have a suitable already, and create a reCAPTCHA Enterprise key. Save the key somewhere and take note of your Cloud project ID while you're there as we'll need that later. You'll find your project ID on the relevant dashboard card. 

Incidentally, you can follow the link to reCAPTCHA Enterprise in the documentation to get to where you need to be in the Google Cloud Platform (or just navigate to Menu > Security > reCAPTCHA Enterprise). 

Once that's done, you may want to hop into Menu > APIs and services. If the reCAPTCHA Enterprise API isn't already enabled, find it in the library and enable it. While you're here, you'll also want to create credentials. This is where things are still a bit blurry for me but I created both an API key and a service account. The API key isn't used in my final code and it's possibly not necessary. I'm just used to API keys, I guess! 

The service account basically receives a name and ID and is then assigned the reCAPTCHA Agent role (locate reCAPTCAHA Enterprise in the list of products and services and then select the reCAPTCHA Agent role). 

Once the service account exists, I chose to create a key ("Manage keys"). Google will warn you about the risks of downloading service account keys and suggests using Workload Identity Federation--I don't yet know what this is so do you own homework here. I created a new key (ADD KEY > Create new key) as a .json file--you'll be prompted to download/save this file and I'd suggest storing a copy in a safe place as I have yet to find a way to download this same file again in future. 

Code

With that setup done, we now move into the code. The code runs clients side and server side and is comprised of your HTML page (including a form), a couple of scripts in the HEAD tag and some attributes on the form button. And of course the PHP code. 

Be sure to add your reCAPTCHA Enterprise key to both the script tag in the head and the data-sitekey attribute on your button. It's also required in the call to create_assessment() in the PHP code. 

I'll point out that, in my case, I want to trigger a score-based reCAPTCHA assessment when a user submits a form--no sooner and not unecessarily. I'm therefore using the relevant Google examples here, combined into a single, cohesive file. 

The HTML and script source is taken from this page: https://cloud.google.com/recaptcha-enterprise/docs/instrument-web-pages 


Note the PHP code is slightly modified to include the putenv call, which references the JSON key file we created earlier. You'll also want to drop in your Google Cloud Platform project ID in the call to create_assessment(). 

<html>
<head>
	<script src="https://www.google.com/recaptcha/enterprise.js?render=your_recaptcha_key"></script>
	<script>
	   function onSubmit(token) {
		 document.getElementById("demo-form").submit();
	   }
	</script>
</head>
<body>
<form id="demo-form" method="post">
<input type="text" />
<button class="g-recaptcha"
data-sitekey="your_recaptcha_key"
data-callback='onSubmit'
data-action='submit'>Submit</button>
</form>
</body>
</html>

<?php

require 'google-re/vendor/autoload.php';

use Google\Cloud\RecaptchaEnterprise\V1\RecaptchaEnterpriseServiceClient;
use Google\Cloud\RecaptchaEnterprise\V1\Event;
use Google\Cloud\RecaptchaEnterprise\V1\Assessment;
use Google\Cloud\RecaptchaEnterprise\V1\TokenProperties\InvalidReason;

/**
* Create an assessment to analyze the risk of a UI action.
* @param string $siteKey The key ID for the reCAPTCHA key (See https://cloud.google.com/recaptcha-enterprise/docs/create-key)
* @param string $token The user's response token for which you want to receive a reCAPTCHA score. (See https://cloud.google.com/recaptcha-enterprise/docs/create-assessment#retrieve_token)
* @param string $project Your Google Cloud project ID
*/
function create_assessment(
  string $siteKey,
  string $token,
  string $project
): void {
	
  // *** I added this line ***
  putenv('GOOGLE_APPLICATION_CREDENTIALS=your-service-account-key-file.json');
    
  // TODO: To avoid memory issues, move this client generation outside
  // of this example, and cache it (recommended) or call client.close()
  // before exiting this method.
  $client = new RecaptchaEnterpriseServiceClient();

  $projectName = $client->projectName($project);

  $event = (new Event())
	  ->setSiteKey($siteKey)
	  ->setToken($token);

  $assessment = (new Assessment())
	  ->setEvent($event);

  try {
	  $response = $client->createAssessment(
		  $projectName,
		  $assessment
	  );

	  // You can use the score only if the assessment is valid,
	  // In case of failures like re-submitting the same token, getValid() will return false
	  if ($response->getTokenProperties()->getValid() == false) {
		  printf('The CreateAssessment() call failed because the token was invalid for the following reason: ');
		  printf(InvalidReason::name($response->getTokenProperties()->getInvalidReason()));
	  } else {
		  printf('The score for the protection action is:');
		  printf($response->getRiskAnalysis()->getScore());

		  // Optional: You can use the following methods to get more data about the token
		  // Action name provided at token generation.
		  // printf($response->getTokenProperties()->getAction() . PHP_EOL);
		  // The timestamp corresponding to the generation of the token.
		  // printf($response->getTokenProperties()->getCreateTime()->getSeconds() . PHP_EOL);
		  // The hostname of the page on which the token was generated.
		  // printf($response->getTokenProperties()->getHostname() . PHP_EOL);
	  }
  } catch (exception $e) {
	  printf('CreateAssessment() call failed with the following error: ');
	  printf($e);
  }
}

   create_assessment(
      'YOUR_RECAPTCHA_SITE_KEY',
      $_POST['g-recaptcha-response'], // Safety first! Do you trust this code?
      'YOUR_GOOGLE_CLOUD_PROJECT_ID'
    );
?>

Drop all of the above in a file named whatever.php and upload it to your web server. Before any of this will run on your server, you'll need to attend to some dependencies. 

Upload your .json key file alongside your .php file (or put it somewhere else on the server and amend the putenv path in the PHP code). This file may warrant additional protections. 

With that sorted, you need to fetch all of the Google Cloud files the PHP above depends on. You'll likely want to do this using Composer (which needs to be installed on your desktop) and you can then run this command in a command window: 

composer require google/cloud-recaptcha-enterprise

I'll note I'm only fetching the reCAPTCHA Enterprise bits here--not the entire Google Cloud file set. You do you. 

I saved all of the Google files in a directory named google-re, which you'll see referenced in the first line our PHP. Adjust as required. 

A final note for the FileZilla users out there: when uploading the Google dependencies in particular, you may encounter a horrible runtime exception ("Fail to push limit") if you don't configure the FileZilla transfer type as Binary (Transfer > Transfer type > Binary). Refer to the answer to this question for more information: https://groups.google.com/g/protobuf/c/8_S93nJWxUE?pli=1

Run It

And now you should be able to request your page, submit the button. All being well, you'll encounter no exceptions and receive a nice meessage like "The score for the protection action is:0.89999997615814". Use that and other, related assessment information to act accordingly.  

I hope that helps someone!


Tuesday, 15 February 2011

How to reference a user control deployed to the GAC

Warning: I don't consider myself a control developer in the classic sense (although I frequently write web parts for deployment within a SharePoint environment, the scope of that deployment is fairly small by default).

A lot of the functionality I build these days runs client-side, powered by my all-time best friend jQuery. Because of this, I find I'm most productive when I first establish the HTML structure in a static HTML file before moving the mark-up to a control. In the bad old days of complete ignorance, I would have had refactor this HTML and build it programmatically within a custom control—an approach I despise (HTML belongs in mark-up).

To overcome many of the drawbacks to custom control development, I prefer migrating my prototype to a user control (which has a front-end .ascx file) and then loading that control in my web part code-behind or custom field control. Check out the LoadControl documentation for information about how load a user control programmatically.

This works beautifully when the control's code behind is compiled to an assembly destined for deployment to the private bin directory. The user control can be built in isolation with the ascx file and code behind remaining wired up for easy access to Intellisense, etc. At build time, the .ascx file is copied somewhere and deployed somewhere useful (the CONTROLTEMPLATES virtual directory, if you like).

Things get trickier when that assembly is to be deployed to the global assembly cache (aka the GAC). In my case I wanted to do this for a custom field control; although most of the code for that control was already being deployed to the GAC, it made sense to keep these artefacts together in the same project. For my non-SharePoint readers, any assembly going into the GAC has to be strong named and signed (via the project's properties sheet > Signing tab); to add the assembly to the GAC, call gacutil:

gacutil –i "MyAsemblyName.dll"

With that out of the way, assume we've got two projects: the first (MyControls) is a class library outputting a signed assembly intended for the GAC; the second is a simple web site (Web).

GAC_User_Control_sln

The MyControls project contains our user control (for information about how to set this up, refer to my post How to add a web project item to a class library). The MyControls assembly is deployed to the GAC.

The web site project contains a copy of the .ascx user control file from the MyControls project and a web page with a @ Register directive pointing to the project-local .ascx file. The Web project doesn't reference the MyControls project because we want it to load the assembly it depends on from the GAC. The .ascx copy can be done manually but you'll likely want to automate this as a pre-build task.

While the MyControls project will now compile, the Web site project will fail to compile with the error: Could not load type 'MyControls.MyUserControl'. If you're in a SharePoint environment, you'll likely see get this as a parser error when the page is dynamically compiled at first request.

To fix this, you need to add an @ Assembly directive to the top of the .ascx file to reference the MyControls assembly deployed to the GAC. You'll need the assembly name and public key token to flesh out this directive. The assembly name can be retrieved from the project properties sheet (normally it's the same as the project anyway). Then extract the public key token using the strong name application (it's the short value):

sn –Tp "MyControls.dll"

If your AssemblyInfo.cs specifies a version number of 1.0.0.0, your @ Assembly directive should look something like this:

<%@ Assembly Name="MyControls, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=b3de351d91c5d4d2"%>

If you version numbers are automatically updated by some kind of policy or build event, beware you'll also need to update this directive as well, which may prove cumbersome.

This directive can be added to both copies of the .ascx file without impacting the MyControls build or edit-time experience.

Both projects will now compile and run, successfully loading the user control base type (i.e. the code behind) from the GAC.

You can download this solution here.

If you found this post helpful, please support my advertisers.

Wednesday, 13 October 2010

SharePoint and Chrome - Better Together

I've been using Google's Chrome browser since its first release in 2008; I've loved nearly every second of the experience. Who would've thought there was room left to innovate in the browser space? Chrome's omnibar and rapid-fire JavaScript rendering, among other tweaks, are simply light years ahead of the competition.

While I normally rely on IE for my MOSS/SharePoint editing interactions, as of late I'm making the switch to Chrome in that space as well. What I've found to date has blown my mind.

Yes the MOSS 2007 UI degrades somewhat but it's still very useable. More importantly, Chrome drastically reduces the time it takes to accomplish basic tasks like modifying page content or viewing list data. I'm not saying these are normally slow in SharePoint but they can be in the www.westernaustralia.com environment (it's an ageing site with a lot of content and a lot of customisations); some pages in particular nearly grind to a halt in IE8 with the corresponding process consuming upwards of 1GB of memory the more I interact with the page.

Chrome "fixes" many of these slowdowns I'd previously attributed to the SharePoint environment and gives me all the Chrome goodness I've come to love over the last two years. It almost makes the SharePoint editing experience pleasurable!

If you found this post helpful, please support my advertisers.

Monday, 13 September 2010

How to pass JSON arrays and other data types to an ASMX web service

Ah interoperability… great fun, great fun.

So jQuery is your new best friend and, along with JSON, there's nuthin' you can't do. The server side stuff is still there in the background and you've got some old school ASP.NET (.asmx) web services hanging around but DOM elements are otherwise flying all over the place, postbacks are just so passé, and even the marketing girls are mildly impressed at your skillz. You're branching out, shifting code and complexity from the server to the browser, and it's time to do some heavier data shunting. Here a few things to know about passing JSON data to an ASMX web service that may help you on your way…

JSON.stringify

Know it, use it, love it. It's part of the JSON2 library and you need it if you don't have it already. Use it to prepare (aka properly encode) your JSON data before sending it off to the big mean ol' web server:

data: {"days": JSON.stringify(["Mon", "Tues"])}

That will encode as &days=["Monday","Tuesday"]

Yeah, I know, it's another file to download but the guy who wrote JSON also wrote this and it can be merged and minified. I've tried writing my own mini-version as a function and while this works for simple strings, save yourself some time when it comes to arrays and the like and just use this sucker.

Arrays

Arrays seem trickier than they should… maybe I'm just a dumb guy—probably. Anyway, you can pass a JSON array to an .asmx web service without much work at all.

The client-side call listed above is everything you need to do from that end. On the server side, create yourself a new web service method with a List<string> parameter:

[WebMethod]
[ScriptMethod (UseHttpGet = true, ResponseFormat = ResponseFormat.Json)]
public string ConsumeArray (List<string> days)
{…}

That's all there is to it. If you're not passing in strings, declare the List<> parameter with a type of object or something else. You can use .NET arrays in the web method signature as well if you really want (need) to.

Integers

When in doubt, stringify:

data: { "i": JSON.stringify(2) }

An int parameter on the web service end will handle this graciously.

Booleans

The good ol' boolean—a simple concept computer science has managed to bastardise like no other…

When in doubt, stringify:

data: { "b": jsonpEncode("true") }

Like the int parameter, a bool in your web method signature will take care of this.

A brief note: JSON, or rather jQuery's parseJSON function, is a particular beast and doesn't seem to know about anything other than the lower case true and false strings. If, for any reason, you ToString a bool in your .NET web service and try to pass it back, parseJSON will fail. If you forget to brush your teeth in the morning, parseJSON will fail.

Dates

Sorry, on my todo list ;-)

Tools

When working through this stuff, it pays to have Fiddler open to inspect the requests you're sending through and any error messages you're getting back. I find Fiddler sometimes breaks this stuff so try turning off the capture if you're getting weird errors; optionally, revert to Firebug (Firefox only, of course).

Fully decoding the data you sniff from a JSONP request passed along in the query string will require some additional tooling; in short, you'll want to decode the value using a free online tool like Opinionated Geek's URL decoder.

If you found this post helpful, please support my advertisers.

Monday, 5 July 2010

jquery.jsonp addresses issues with beforeSend and timeout

On the coat-tails of my previous post, I've come across the Google Code jquery-jsonp library and it's a saviour if you're working JSONP requests. Remember a JSONP request dynamically inserts a script tag into the DOM and doesn't use the same XMLHttpRequest as your typical AJAX request.

I ended up here because—much like beforeSend—the timeout parameter wasn't having any effect on my jsonp request: the request simply wouldn't time out despite the value supplied to this parameter.

In addition to supporting both beforeSend and timeout properties, jquery-jsonp adds some really nice caching support—both typical HTTP (aka "browser") caching and page-based caching that leverages the JavaScript VM's memory (apparently). Anyway it seems to work as advertised. Perhaps most importantly, the name of the callback can be changed.

A minified version of the code is available from Google Code and the documentation is concise and well-written with examples.