diff --git a/.eslintignore b/.eslintignore index 9340ad9b86..6173a70be8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ test/fixtures build/ docs/ protos/ +static/ \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index dcd7d19ee8..11c2d15619 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ compute @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-revie iam @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers kms @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers orgpolicy @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers +recaptcha @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers secret-manager @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers security-center @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers service-directory @GoogleCloudPlatform/dee-infra @GoogleCloudPlatform/nodejs-samples-reviewers diff --git a/recaptcha/demosite/.docker-compose.yml b/recaptcha/demosite/.docker-compose.yml new file mode 100644 index 0000000000..a02450512d --- /dev/null +++ b/recaptcha/demosite/.docker-compose.yml @@ -0,0 +1,16 @@ +version: "3" +services: + livereload: + image: demosite-livereload + build: + context: "app" + dockerfile: Dockerfile + args: + - "GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT}" + - "SITE_KEY=${SITE_KEY}" + - "CHECKBOX_SITE_KEY=${CHECKBOX_SITE_KEY}" + command: node index.js + ports: ["8000:8000"] + volumes: + - "./app:/app" + restart: always diff --git a/recaptcha/demosite/README.md b/recaptcha/demosite/README.md new file mode 100644 index 0000000000..508f5f4188 --- /dev/null +++ b/recaptcha/demosite/README.md @@ -0,0 +1,96 @@ +# Google Cloud reCAPTCHA Enterprise + +Google [Cloud reCAPTCHA Enterprise](https://cloud.google.com/recaptcha-enterprise) helps protect your website from fraudulent activity, spam, and abuse without creating friction. + +# Google Cloud reCAPTCHA Enterprise + +## Prerequisites + +### Google Cloud Project + +Set up a Google Cloud project. +Billing information is **not needed** to deploy this application. + +# One-click deploy + +1. Click the below "Open in Cloud Shell" button. + + +Open Cloud Shell + +2. Run +``` +cd samples/demosite/app && sh init.sh +``` + +3. Click on the localhost link in the terminal output. You'll find the deployed application. + + +# Manual Deploy + +### 1. Enable the reCAPTCHA Enterprise API + +You must [enable the Google reCAPTCHA Enterprise API](https://console.cloud.google.com/flows/enableapi?apiid=recaptchaenterprise.googleapis.com) for your project in order to use this application. + +### 2. Create Score key and Checkbox key + +Create a Score key, and a Checkbox key via [Cloud Console.](https://console.cloud.google.com/security/recaptcha) + +### 3. Set Environment Variables + +Open the CloudShell from Cloud Console. +Set your project ID and site keys. + +```angular2html +export GOOGLE_CLOUD_PROJECT="" +export SITE_KEY="" +export CHECKBOX_SITE_KEY="" +``` + +### 4. Clone, Build and Run + +The following instructions will help you prepare your development environment. + + +1. Clone the nodejs-recaptcha-enterprise repository and navigate to ```samples/demosite``` directory. + +``` +cloudshell_open --repo_url "https://github.com/googleapis/nodejs-recaptcha-enterprise.git" --dir "samples/demosite" --page "shell" --force_new_clone --git_branch "demosite" +``` + +2. Run docker-compose + +``` +/usr/local/bin/docker-compose -f $PWD/docker-compose.yaml up --build +``` + +3. Click on the localhost link in the terminal output. You'll find the deployed application. + +## Authentication + +The above _**one-click**_ and _**manual**_ deployment works with the default **compute-engine** service account in the project. +If you want to create a new service account, follow the below steps. + +### 1. Create Service account + +A service account with private key credentials is required to create signed bearer tokens. + +Create +1. [Service account](https://console.cloud.google.com/iam-admin/serviceaccounts/create) +2. [Key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-console) for the service account +3. Download the key credentials file as JSON. + +### 2. Grant Permissions + +You must ensure that the [user account or service account](https://cloud.google.com/iam/docs/service-accounts#differences_between_a_service_account_and_a_user_account) you used to authorize your gcloud session has the proper permissions to edit reCAPTCHA Enterprise resources for your project. In the Cloud Console under IAM, add the following roles to the project whose service account you're using to test: + +* reCAPTCHA Enterprise Agent +* reCAPTCHA Enterprise Admin + +More information can be found in the [Google reCAPTCHA Enterprise Docs](https://cloud.google.com/recaptcha-enterprise/docs/access-control#rbac_iam). + +### 3. Export the Service account credentials + +```angular2html +export GOOGLE_APPLICATION_CREDENTIALS="" +``` diff --git a/recaptcha/demosite/app/Dockerfile b/recaptcha/demosite/app/Dockerfile new file mode 100644 index 0000000000..f063624423 --- /dev/null +++ b/recaptcha/demosite/app/Dockerfile @@ -0,0 +1,18 @@ +FROM node:16-slim + +ARG GOOGLE_CLOUD_PROJECT +ENV GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT} + +ARG SITE_KEY +ENV SITE_KEY=${SITE_KEY} + +ARG CHECKBOX_SITE_KEY +ENV CHECKBOX_SITE_KEY=${CHECKBOX_SITE_KEY} + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +# Install production dependencies. +RUN npm install diff --git a/recaptcha/demosite/app/controllers/assessmentController.js b/recaptcha/demosite/app/controllers/assessmentController.js new file mode 100644 index 0000000000..35bccc606e --- /dev/null +++ b/recaptcha/demosite/app/controllers/assessmentController.js @@ -0,0 +1,26 @@ +const {createAssessment} = require('../recaptcha/createAssessment'); + +const assessmentController = async (req, res) => { + try { + const assessmentData = await createAssessment( + process.env.GOOGLE_CLOUD_PROJECT, + req.body.sitekey, + req.body.token, + req.body.action + ); + + res.json({ + error: null, + data: assessmentData, + }); + } catch (e) { + res.json({ + error: e.toString(), + data: null, + }); + } +}; + +module.exports = { + assessmentController, +}; diff --git a/recaptcha/demosite/app/controllers/loginController.js b/recaptcha/demosite/app/controllers/loginController.js new file mode 100644 index 0000000000..23201af7a8 --- /dev/null +++ b/recaptcha/demosite/app/controllers/loginController.js @@ -0,0 +1,12 @@ +const loginController = (req, res) => { + const context = { + project_id: process.env.GOOGLE_CLOUD_PROJECT, + site_key: process.env.SITE_KEY, + }; + + res.render('login', context); +}; + +module.exports = { + loginController, +}; diff --git a/recaptcha/demosite/app/controllers/signupController.js b/recaptcha/demosite/app/controllers/signupController.js new file mode 100644 index 0000000000..68ec096fa1 --- /dev/null +++ b/recaptcha/demosite/app/controllers/signupController.js @@ -0,0 +1,12 @@ +const signupController = (req, res) => { + const context = { + project_id: process.env.GOOGLE_CLOUD_PROJECT, + checkbox_site_key: process.env.CHECKBOX_SITE_KEY, + }; + + res.render('signup', context); +}; + +module.exports = { + signupController, +}; diff --git a/recaptcha/demosite/app/index.js b/recaptcha/demosite/app/index.js new file mode 100644 index 0000000000..acf01bacf3 --- /dev/null +++ b/recaptcha/demosite/app/index.js @@ -0,0 +1,26 @@ +const express = require('express'); +const mustacheExpress = require('mustache-express'); +const bodyParser = require('body-parser'); + +const router = require('./routes'); + +const app = express(); +const port = 8000; + +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); +app.use(bodyParser.json()); + +app.engine('html', mustacheExpress()); +app.set('view engine', 'html'); +app.set('views', __dirname + '/templates'); + +app.use('/static', express.static('static')); +app.use('/', router); + +app.listen(port, () => { + console.log(`Recaptcha demosite app listening on port ${port}`); +}); diff --git a/recaptcha/demosite/app/init.sh b/recaptcha/demosite/app/init.sh new file mode 100644 index 0000000000..03d661bb8d --- /dev/null +++ b/recaptcha/demosite/app/init.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# gcloud command to get the current GOOGLE Project id. +export GOOGLE_CLOUD_PROJECT=$(gcloud config list --format 'value(core.project)' 2>/dev/null) +gcloud config set project "$GOOGLE_CLOUD_PROJECT" + +# Enabling the reCAPTCHA Enterprise API +gcloud services enable recaptchaenterprise.googleapis.com + +# gcloud command to create reCAPTCHA keys. +gcloud alpha recaptcha keys create --display-name=demo-recaptcha-score-key --web --allow-all-domains --integration-type=SCORE 1>/dev/null 2>recaptchascorekeyfile +export SITE_KEY=$(cat recaptchascorekeyfile | sed -n -e 's/.*Created \[\([0-9a-zA-Z_-]\+\)\].*/\1/p') +gcloud alpha recaptcha keys create --display-name=demo-recaptcha-checkbox-key --web --allow-all-domains --integration-type=CHECKBOX 1>/dev/null 2>recaptchacheckboxkeyfile +export CHECKBOX_SITE_KEY=$(cat recaptchacheckboxkeyfile | sed -n -e 's/.*Created \[\([0-9a-zA-Z_-]\+\)\].*/\1/p') + +# Docker compose up +DOCKER_COMPOSE="/usr/local/bin/docker-compose -f $HOME/cloudshell_open/nodejs-recaptcha-enterprise/samples/demosite/docker-compose.yaml up --build" +$DOCKER_COMPOSE +DOCKER_COMPOSE_RESULT=$? +if [[ $DOCKER_COMPOSE_RESULT == *"error"* ]]; +then + echo "Deployment error" +fi diff --git a/recaptcha/demosite/app/package.json b/recaptcha/demosite/app/package.json new file mode 100644 index 0000000000..aea82b880a --- /dev/null +++ b/recaptcha/demosite/app/package.json @@ -0,0 +1,19 @@ +{ + "name": "recaptcha-demosite", + "description": "", + "version": "1.0.0", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/googleapis/nodejs-recaptcha-enterprise.git" + }, + "license": "MIT", + "dependencies": { + "@google-cloud/recaptcha-enterprise": "^3.0.0", + "body-parser": "^1.20.0", + "express": "^4.18.1", + "mustache-express": "^1.3.2" + } +} diff --git a/recaptcha/demosite/app/recaptcha/createAssessment.js b/recaptcha/demosite/app/recaptcha/createAssessment.js new file mode 100644 index 0000000000..44af4a3971 --- /dev/null +++ b/recaptcha/demosite/app/recaptcha/createAssessment.js @@ -0,0 +1,63 @@ +const {RecaptchaEnterpriseServiceClient} = + require('@google-cloud/recaptcha-enterprise').v1; + +const THRESHHOLD_SCORE = 0.5; + +async function createAssessment( + projectId, + recaptchSiteKey, + token, + recaptchaAction +) { + const client = new RecaptchaEnterpriseServiceClient(); + + const [response] = await client.createAssessment({ + parent: `projects/${projectId}`, + assessment: { + event: { + siteKey: recaptchSiteKey, + token, + }, + }, + }); + + // Check if the token is valid. + if (!response.tokenProperties || !response.tokenProperties.valid) { + throw new Error( + `The Create Assessment call failed because the token was invalid for the following reasons: ${response.tokenProperties.invalidReason}` + ); + } + + // Check if the expected action was executed. + if (response.tokenProperties.action !== recaptchaAction) { + throw new Error( + 'The action attribute in your reCAPTCHA tag does not match the action you are expecting to score. Please check your action attribute !' + ); + } + + // Get the risk score and the reason(s) + // For more information on interpreting the assessment, + // see https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment + for (const reason of response.riskAnalysis.reasons) { + console.log(reason); + } + + console.log( + `The reCAPTCHA score for this token is: ${response.riskAnalysis.score}` + ); + + let verdict = 'Human'; + + if (response.riskAnalysis.score < THRESHHOLD_SCORE) { + verdict = 'Not a human'; + } + + return { + score: response.riskAnalysis.score, + verdict, + }; +} + +module.exports = { + createAssessment, +}; diff --git a/recaptcha/demosite/app/routes.js b/recaptcha/demosite/app/routes.js new file mode 100644 index 0000000000..90a8274a2d --- /dev/null +++ b/recaptcha/demosite/app/routes.js @@ -0,0 +1,12 @@ +const express = require('express'); +const router = express.Router(); + +const {loginController} = require('./controllers/loginController'); +const {signupController} = require('./controllers/signupController'); +const {assessmentController} = require('./controllers/assessmentController'); + +router.get('/login', loginController); +router.get('/signup', signupController); +router.post('/create_assessment', assessmentController); + +module.exports = router; diff --git a/recaptcha/demosite/app/static/assessment.js b/recaptcha/demosite/app/static/assessment.js new file mode 100644 index 0000000000..c8ee57ae19 --- /dev/null +++ b/recaptcha/demosite/app/static/assessment.js @@ -0,0 +1,94 @@ +const createAssessmentRequest = (token, action, sitekey) => { + return fetch('/create_assessment', { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({token, action, sitekey}), + }); +}; + +function assessRecaptcha(element, event) { + event.preventDefault(); + grecaptcha.enterprise.ready(() => { + grecaptcha.enterprise + .execute(element.getAttribute('data-sitekey'), { + action: element.getAttribute('data-action'), + }) + .then(token => { + return createAssessmentRequest( + token, + element.getAttribute('data-action'), + element.getAttribute('data-sitekey') + ); + }) + .then(res => res.json()) + .then(res => { + if (!res) { + addMessage('Error: request without data'); + return; + } + + if (!res.error && res.data) { + document.getElementById('scoreButton').innerText = res.data.score; + return; + } + + if (res.error) { + addMessage(`Server error: ${res.error}`); + } + }) + .catch(res => { + addMessage(`Server error: ${res}`); + }); + }); +} + +const verifyCallback = function (token) { + const recaptchaDiv = document.getElementById('recaptcha_render_div'); + + createAssessmentRequest( + token, + recaptchaDiv.getAttribute('data-action'), + recaptchaDiv.getAttribute('data-sitekey') + ) + .then(res => res.json()) + .then(res => { + if (!res) { + addMessage('Error: request without data'); + return; + } + + if (!res.error && res.data) { + document.getElementById('scoreButton').innerText = res.data.score; + return; + } + + if (res.error) { + addMessage(`Server error: ${res.error}`); + } + }) + .catch(res => { + addMessage(`Server error: ${res}`); + }); +}; + +const onloadCallback = function (event) { + event.preventDefault(); + document.getElementById('submitBtn').style.display = 'none'; + const recaptcha_render_div = document.getElementById('recaptcha_render_div'); + recaptcha_render_div.style.display = 'block'; + grecaptcha.enterprise.render(recaptcha_render_div, { + sitekey: recaptcha_render_div.getAttribute('data-sitekey'), + callback: verifyCallback, + theme: 'light', + }); +}; + +function addMessage(message) { + const myDiv = document.createElement('div'); + myDiv.id = 'div_id'; + myDiv.innerHTML = message + '

'; + document.body.appendChild(myDiv); +} diff --git a/recaptcha/demosite/app/static/home.css b/recaptcha/demosite/app/static/home.css new file mode 100644 index 0000000000..25f46e9096 --- /dev/null +++ b/recaptcha/demosite/app/static/home.css @@ -0,0 +1,52 @@ +body{ + height: 800px; + display: flow; + align-items: center; + font-family: "Google Sans", fantasy; +} + +#scoreButton{ + background: #f3f2f2; + width: 20%; + text-transform: uppercase; + font-family: "Google Sans", fantasy; + font-size: 1em; + height: 30px; +} + +#user-img{ + margin: auto; + position: relative; + top: -35px; + max-width: 200px; + max-height: 100px; +} + +#submitBtn{ + background: #4285f4; + width: 35%; + font-family: "Google Sans", fantasy; + font-weight: bold; + font-size: 1.25em; + height: 55px; +} + +a{ + color: #4285f4 !important; +} + +.card-panel{ + text-align: center; +} + +.card-panel h3{ + + font-family: "Google Sans", fantasy; + font-weight: 200 !important; + margin-top: -1em; + +} + +#recaptcha_render_div{ + display: none; +} diff --git a/recaptcha/demosite/app/static/recaptcha.png b/recaptcha/demosite/app/static/recaptcha.png new file mode 100644 index 0000000000..d4210b98f6 Binary files /dev/null and b/recaptcha/demosite/app/static/recaptcha.png differ diff --git a/recaptcha/demosite/app/templates/login.html b/recaptcha/demosite/app/templates/login.html new file mode 100644 index 0000000000..1c41145fa9 --- /dev/null +++ b/recaptcha/demosite/app/templates/login.html @@ -0,0 +1,60 @@ + + + + + reCAPTCHA-Enterprise + + + + + + + + +

+
+
+
+ reCAPTCHA logo +

Score Key: Demo

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ Don't have an account yet? +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + diff --git a/recaptcha/demosite/app/templates/signup.html b/recaptcha/demosite/app/templates/signup.html new file mode 100644 index 0000000000..ba8d7823b7 --- /dev/null +++ b/recaptcha/demosite/app/templates/signup.html @@ -0,0 +1,77 @@ + + + + + reCAPTCHA-Enterprise + + + + + + + + +
+
+
+
+ reCAPTCHA logo +

Checkbox Key: Demo

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ Already have an account? +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ +