Skip to content

Instantly share code, notes, and snippets.

@tanmaiaccion
Forked from nicosabena/redirect-rule.js
Created September 2, 2020 20:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tanmaiaccion/1dbc6cb1b2329191d97272d54afbebfc to your computer and use it in GitHub Desktop.
Save tanmaiaccion/1dbc6cb1b2329191d97272d54afbebfc to your computer and use it in GitHub Desktop.
Redirect rule + webtask to do a reCaptcha after authentication
function (user, context, callback) {
// this rule requires the following configuration values:
// CAPTCHA_SECRET: a 32 bytes string that will be the shared secret between
// the rule and the webtask
// AUTH0_DOMAIN: your auth0 domain (e.g. account.auth0.com)
// CAPTCHA_REDIRECT: the URL for the webtask that will show and process CAPTCHA
// Put a specific client ID if you dont want CAPTCHA for every client
// if (context.clientID !== '[your client id]')
// return callback(null, user, context);
var secret = configuration.CAPTCHA_SECRET;
var ruleUri = 'https://' + configuration.AUTH0_DOMAIN + '/rules/captcha';
var wtUri = ruleUri + '/wt';
if (context.protocol !== "redirect-callback") {
// User has initiated a login and is forced to take a CAPTCHA challenge
// Send user's information in a JWT to avoid tampering
var token = jwt.sign({
sub: user.user_id,
clientName: context.clientName
},
secret,
{
expiresInMinutes: 5,
audience: wtUri,
issuer: ruleUri
}
);
var redirectUrl = configuration.CAPTCHA_REDIRECT;
var separator = redirectUrl.indexOf('?') !== -1 ? "&" : "?";
context.redirect = {
url: redirectUrl + separator + "token=" + token + "&webtask_no_cache=1"
};
return callback(null, user, context);
} else {
// User has been redirected to /continue?token=...,
// captcha action must be validated
// The generated token must include a `captchaOk` claim to
// confirm a successful response
var postVerify = function(err, decoded) {
if (err) {
return callback(new UnauthorizedError("Error validating token from wt: " + err));
} else if (decoded.sub !== user.user_id) {
return callback(new UnauthorizedError("Token does not match the current user."));
} else if (!decoded.captchaOk) {
return callback(new UnauthorizedError("Captcha validation was not successful.\n" +
decoded.errorMessage || ""));
} else {
// Captcha ok, go ahead with authentication
return callback(null, user, context);
}
};
jwt.verify(
context.request.query.token,
secret,
{
audience: ruleUri,
issuer: wtUri
},
postVerify
);
}
}
'use latest';
// This webtask requires the following configured secrets:
// RECAPTCHA_SITE_KEY These two values can be obtained from http://www.google.com/recaptcha/admin
// RECAPTCHA_SITE_SECRET
//
// CAPTCHA_SECRET A 32 byte string that is the shared key between the rule and the webtask
// AUTH0_DOMAIN Your Auth0 domain (e.g. account.auth0.com)
import { fromExpress } from 'webtask-tools';
const express = require('express@4.14.0');
const bodyParser = require('body-parser@1.12.4');
const app = express();
const url = require('url'); // built-in utility
const jwt = require('jsonwebtoken@5.7.0');
const request = require('request@2.67.0');
app.use(bodyParser.urlencoded()); // for parsing application/x-www-form-urlencoded
var getRuleUri = function(req) { return 'https://' + req.webtaskContext.secrets.AUTH0_DOMAIN + '/rules/captcha'; };
var getWtUri = function(req) { return getRuleUri(req) + '/wt';};
app.get('/', (req, res) => {
var captchaView = (function view() {/*
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<STYLE type="text/css">
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body { margin: 0; }
.container { padding-right: 15px; padding-left: 15px;margin-right: auto; margin-left: auto; }
h1 { margin: .67em 0; font-size: 2em; font-family: inherit; font-weight: 500; line-height: 1.1; color: inherit; }
</STYLE>
<title>Confirm you are human</title>
<!-- Latest compiled and minified CSS -->
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body>
<script type="text/javascript">
var submitform = function() {
document.getElementById("captchaform").submit();
};
</script>
<div class="container">
<form class="form-signin" action="<%= target %>" method="POST" id="captchaform">
<h1>Complete the log-in process <%= clientName %></h1>
<input type="hidden" value="<%= state %>" name="state" />
<input type="hidden" value="<%= token %>" name="token" />
<div class="g-recaptcha" data-sitekey="<%= sitekey %>" data-callback="submitform"></div>
</form>
</div>
</body>
</html>
*/}).toString().match(/[^]*\/\*([^]*)\*\/\s*\}$/)[1];
var myUri = req.originalUrl.split("?")[0];
var secrets = req.webtaskContext.secrets;
jwt.verify(
req.body.token,
secrets.CAPTCHA_SECRET, {
audience: getWtUri(req),
issuer: getRuleUri(req)
},
function(err, tokenPayload) {
var target = url.parse(req.originalUrl).pathname;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(require('ejs').render(captchaView, {
state: req.query.state,
target: target,
token: req.query.token,
sitekey: secrets.RECAPTCHA_SITE_KEY,
clientName: tokenPayload ? "to " + tokenPayload.clientName : ""
}));
});
});
app.post('/', (req, res) => {
var secrets = req.webtaskContext.secrets;
var continueToRule = function(err, subject, state) {
var token = jwt.sign({
captchaOk: err === null,
sub: subject,
errorMessage: err
},
secrets.CAPTCHA_SECRET, {
expiresInMinutes: 2,
audience: getRuleUri(req),
issuer: getWtUri(req)
}
);
res.redirect(302, "https://" + secrets.AUTH0_DOMAIN + "/continue?state=" + req.body.state + "&token=" + token);
}
var postVerify = function(err, decoded) {
if (err) {
continueToRule("Invalid token: " + err, null);
} else if (!decoded.sub) {
continueToRule("Token does not contain 'sub' claim.", null);
} else {
continueToRule(null, decoded.sub);
}
};
var verifyCaptcha = function(captchaResponse, callback) {
request.post( {
url: 'https://www.google.com/recaptcha/api/siteverify',
form: {
secret: secrets.RECAPTCHA_SITE_SECRET,
response: captchaResponse,
remoteip: req.ip
}}, function (error, response, body) {
if (error) {
callback(error);
}
if (response.statusCode !== 200) {
callback('Error validating captcha: '+ response.statusCode);
}
var data = JSON.parse(body);
if (data.success) {
callback();
} else {
callback("Error from reCaptcha: " + JSON.stringify(data));
}
}
);
};
verifyCaptcha(req.body["g-recaptcha-response"], function(err) {
if (err) {
continueToRule("Invalid reCAPTCHA validation." + err, null);
return;
}
jwt.verify(req.body.token, secrets.CAPTCHA_SECRET, {
audience: getWtUri(req),
issuer: getRuleUri(req)
}, postVerify);
});
});
module.exports = fromExpress(app);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment