___TERMS_OF_SERVICE___ By creating or modifying this file you agree to Google Tag Manager's Community Template Gallery Developer Terms of Service available at https://developers.google.com/tag-manager/gallery-tos (or such other URL as Google may provide), as modified from time to time. ___INFO___ { "type": "CLIENT", "id": "cvt_temp_public_id", "version": 1, "__wm": "VGVtcGxhdGUtQXV0aG9yX3JlQ0FQVENIQXYzLVNpbW8tQWhhdmE\u003d", "categories": [ "UTILITY" ], "securityGroups": [], "displayName": "reCAPTCHA v3", "brand": { "id": "brand_dummy", "displayName": "", "thumbnail": "data:image/png;base64,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\u003d\u003d" }, "description": "Template for loading the reCAPTCHA v3 JavaScript as well as for verifying reCAPTCHA requests and generating a bot score.", "containerContexts": [ "SERVER" ] } ___TEMPLATE_PARAMETERS___ [ { "type": "TEXT", "name": "siteKey", "displayName": "Site key", "simpleValueType": true, "help": "Set the *site* key to verify valid requests for api.js.", "valueValidators": [ { "type": "NON_EMPTY" } ], "alwaysInSummary": true }, { "type": "TEXT", "name": "secretKey", "displayName": "Secret key", "simpleValueType": true, "help": "Set the *secret* key to validate communication between the server and reCAPTCHA.", "valueValidators": [ { "type": "NON_EMPTY" } ], "alwaysInSummary": true }, { "type": "CHECKBOX", "name": "setCookie", "checkboxText": "Write bot score in a cookie", "simpleValueType": true, "defaultValue": true }, { "type": "CHECKBOX", "name": "returnResponse", "checkboxText": "Return the bot score in the HTTP response", "simpleValueType": true, "defaultValue": false }, { "type": "CHECKBOX", "name": "setEncodingHeader", "checkboxText": "Set Content-Encoding header (required for Cloud Run)", "simpleValueType": true, "help": "If you check this, a Content-Encoding header with the value \"gzip\" will be sent back in the response to the browser." }, { "type": "GROUP", "name": "cookieSettings", "displayName": "Cookie settings", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "TEXT", "name": "cookieName", "displayName": "Cookie name", "simpleValueType": true, "defaultValue": "__r_b_s", "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "type": "TEXT", "name": "maxAge", "displayName": "Cookie lifetime", "simpleValueType": true, "valueUnit": "seconds", "defaultValue": 2592000, "valueValidators": [ { "type": "NON_EMPTY" }, { "type": "NON_NEGATIVE_NUMBER" } ], "help": "Set the cookie lifetime in seconds (30 days by default). If you set this to 0, a session cookie is created." }, { "type": "CHECKBOX", "name": "httpOnly", "checkboxText": "HttpOnly cookie", "simpleValueType": true, "defaultValue": true } ], "enablingConditions": [ { "paramName": "setCookie", "paramValue": true, "type": "EQUALS" } ] } ] ___SANDBOXED_JS_FOR_SERVER___ // Load APIs const claimRequest = require('claimRequest'); const encodeUriComponent = require('encodeUriComponent'); const getRemoteAddress = require('getRemoteAddress'); const getRequestHeader = require('getRequestHeader'); const getRequestMethod = require('getRequestMethod'); const getRequestQueryParameter = require('getRequestQueryParameter'); const getRequestPath = require('getRequestPath'); const getTimestampMillis = require('getTimestampMillis'); const JSON = require('JSON'); const logToConsole = require('logToConsole'); const makeString = require('makeString'); const returnResponse = require('returnResponse'); const sendHttpGet = require('sendHttpGet'); const sendHttpRequest = require('sendHttpRequest'); const setCookie = require('setCookie'); const setResponseBody = require('setResponseBody'); const setResponseHeader = require('setResponseHeader'); const setResponseStatus = require('setResponseStatus'); const templateDataStorage = require('templateDataStorage'); // Helpers const now = getTimestampMillis(); const oneDayAgo = getTimestampMillis() - 1000 * 60 * 60 * 24; const requestPath = getRequestPath(); const encode = str => { return encodeUriComponent(makeString(str || '')); }; const log = msg => { logToConsole('RECAPTCHA: ' + msg); }; const fail = msg => { log(msg); proxyResponse(msg, {}, 500); }; const proxyResponse = (response, headers, statusCode) => { setResponseStatus(statusCode); setResponseBody(response); for (const key in headers) { setResponseHeader(key, headers[key]); } setResponseHeader('access-control-allow-credentials', 'true'); setResponseHeader('access-control-allow-origin', getRequestHeader('origin')); if (data.setEncodingHeader) { setResponseHeader('Content-Encoding', 'gzip'); } returnResponse(); }; // Claim requests to /recaptcha/ if (requestPath === '/recaptcha' || requestPath === '/recaptcha/' || (requestPath === '/recaptcha/api.js' && getRequestQueryParameter('render') === data.siteKey)) { log('Claiming valid request.'); claimRequest(); // Process request for api.js if (requestPath === '/recaptcha/api.js') { log('Processing request for api.js'); if (!templateDataStorage.getItemCopy('__recaptcha_js') || templateDataStorage.getItemCopy('__recaptcha_ts') < oneDayAgo) { log('Fetching api.js from Google servers.'); sendHttpGet('https://www.google.com/recaptcha/api.js?render=' + data.siteKey, (statusCode, headers, body) => { if (statusCode === 200) { templateDataStorage.setItemCopy('__recaptcha_js', body); templateDataStorage.setItemCopy('__recaptcha_headers', headers); templateDataStorage.setItemCopy('__recaptcha_ts', now); } proxyResponse(body, headers, statusCode); }, {timeout: 5000}); } else { log('Loading api.js from template data cache.'); proxyResponse( templateDataStorage.getItemCopy('__recaptcha_js'), templateDataStorage.getItemCopy('__recaptcha_headers'), 200 ); } // Process requests for the reCAPTCHA API } else { log('Verifying user token and fetching bot score.'); const token = getRequestQueryParameter('token'); if (!token) return fail('Missing token from request.'); const postBody = 'secret=' + encode(data.secretKey) + '&response=' + encode(token) + '&remoteip=' + encode(getRemoteAddress()); sendHttpRequest('https://www.google.com/recaptcha/api/siteverify', (statusCode, headers, body) => { const response = JSON.parse(body || '{}'); if (response.success) { if (data.setCookie) { const cookieOptions = { httpOnly: data.httpOnly, domain: 'auto', secure: true }; if (data.maxAge !== '0') cookieOptions['max-age'] = data.maxAge; setCookie( data.cookieName, makeString(response.score), cookieOptions ); } const responseMsg = data.returnResponse ? JSON.stringify({status: 'success', score: response.score}) : 'success'; proxyResponse(responseMsg, {}, 200); } else { fail(JSON.stringify({status: 'failure', errorCodes: data['error-codes']})); } }, {method: 'POST', timeout: 2000, headers: {'content-type': 'application/x-www-form-urlencoded'}}, postBody); } } ___SERVER_PERMISSIONS___ [ { "instance": { "key": { "publicId": "read_request", "versionId": "1" }, "param": [ { "key": "queryParametersAllowed", "value": { "type": 8, "boolean": true } }, { "key": "headerWhitelist", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "headerName" } ], "mapValue": [ { "type": 1, "string": "forwarded" } ] }, { "type": 3, "mapKey": [ { "type": 1, "string": "headerName" } ], "mapValue": [ { "type": 1, "string": "x-forwarded-for" } ] }, { "type": 3, "mapKey": [ { "type": 1, "string": "headerName" } ], "mapValue": [ { "type": 1, "string": "origin" } ] } ] } }, { "key": "remoteAddressAllowed", "value": { "type": 8, "boolean": true } }, { "key": "headersAllowed", "value": { "type": 8, "boolean": true } }, { "key": "pathAllowed", "value": { "type": 8, "boolean": true } }, { "key": "queryParameterAccess", "value": { "type": 1, "string": "specific" } }, { "key": "requestAccess", "value": { "type": 1, "string": "specific" } }, { "key": "queryParameterWhitelist", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "queryParameter" } ], "mapValue": [ { "type": 1, "string": "render" } ] }, { "type": 3, "mapKey": [ { "type": 1, "string": "queryParameter" } ], "mapValue": [ { "type": 1, "string": "token" } ] } ] } }, { "key": "headerAccess", "value": { "type": 1, "string": "specific" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "return_response", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "logging", "versionId": "1" }, "param": [ { "key": "environments", "value": { "type": 1, "string": "debug" } } ] }, "isRequired": true }, { "instance": { "key": { "publicId": "set_cookies", "versionId": "1" }, "param": [ { "key": "allowedCookies", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "name" }, { "type": 1, "string": "domain" }, { "type": 1, "string": "path" }, { "type": 1, "string": "secure" }, { "type": 1, "string": "session" } ], "mapValue": [ { "type": 1, "string": "*" }, { "type": 1, "string": "*" }, { "type": 1, "string": "*" }, { "type": 1, "string": "any" }, { "type": 1, "string": "any" } ] } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "access_template_storage", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "access_response", "versionId": "1" }, "param": [ { "key": "writeResponseAccess", "value": { "type": 1, "string": "any" } }, { "key": "writeHeaderAccess", "value": { "type": 1, "string": "specific" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "send_http", "versionId": "1" }, "param": [ { "key": "allowedUrls", "value": { "type": 1, "string": "specific" } }, { "key": "urls", "value": { "type": 2, "listItem": [ { "type": 1, "string": "https://www.google.com/recaptcha/*" } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true } ] ___TESTS___ scenarios: - name: Request not claimed with invalid site key code: |- mock('getRequestPath', () => '/recaptcha/api.js?render=invalid_' + mockData.siteKey); runCode(mockData); assertApi('claimRequest').wasNotCalled(); setup: |- const mockData = { siteKey: 'siteKey', secretKey: 'secretKey', setCookie: true, returnResponse: true, cookieName: 'cookieName', maxAge: '1', httpOnly: true }; ___NOTES___ Created on 09/03/2021, 14:26:06