Skip to content

Instantly share code, notes, and snippets.

@eugeniop
Created August 25, 2015 17:39
Show Gist options
  • Save eugeniop/5ec965f109c01277255d to your computer and use it in GitHub Desktop.
Save eugeniop/5ec965f109c01277255d to your computer and use it in GitHub Desktop.
var request = require('request');
var qs = require('qs');
var jwt = require('jsonwebtoken');
return function (context, req, res) {
require('async').series([
/*
* We only care about POST and GET
*/
function(callback) {
if (req.method !== 'POST' && req.method !== 'GET') {
res.writeHead(404);
return res.end('Page not found');
}
return callback();
},
/*
* 1. GET: Render initial View with OTP.
*/
function(callback) {
if (req.method === 'GET') {
renderOtpView();
}
return callback();
},
/*
* 2. Validate OTP
*/
function(callback) {
if (req.method === 'POST') {
yubico_validate(context.data.yubikey_clientid, context.body.otp, function(err,resp) {
if (err) {
return callback(err);
}
if(resp.status==='OK'){
//Return result to Auth0 (includes OTP and Status. Only when OK)
var token = jwt.sign({
status: resp.status,
otp: resp.otp
},
new Buffer(context.data.yubikey_secret, 'base64'),
{
subject: context.data.user,
expiresInMinutes: 1,
audience: context.data.yubikey_clientid,
issuer: 'urn:auth0:yubikey:mfa'
});
res.writeHead(301, {Location: context.data.returnUrl + "?id_token=" + token});
res.end();
}
return callback([resp.status]);
});
return callback();
}
},
], function(err) {
if (Array.isArray(err)) {
return renderOtpView(err);
}
if (typeof err === 'string') {
return renderOtpView([err]);
}
if (typeof err === 'object') {
var errors = [];
errors.push(err.message || err);
return renderOtpView(errors);
}
});
function yubico_validate(clientId, otp, done){
var params = {
id: clientId,
otp: otp,
nonce: uid(16)
};
request.get('http://api.yubico.com/wsapi/2.0/verify',
{
qs: params
},function(e,r,b){
if(e) return done(e);
if(r.statusCode !== 200) return done(new Error('Error: ' + r.statusCode));
var yubico_response=qs.parse(b.replace(/\r\n/g, '&'));
if(yubico_response.nonce !== params.nonce) return done(new Error('Invalid response - nonce doesn\'t match'));
done(null,yubico_response);
});
}
function uid(len) {
var buf = []
, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
, charlen = chars.length;
for (var i = 0; i < len; ++i) {
buf.push(chars[getRandomInt(0, charlen - 1)]);
}
return buf.join('');
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function renderOtpView(errors) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(require('ejs').render(otpForm.stringify(), {
user: context.data.user,
errors: errors || []
}));
}
function otpForm() {
/*
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<title>Auth0 - Yubikey MFA</title>
<style>html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}@font-face{font-family:'avenir roman';src:url("https://cdn.auth0.com/fonts/avenir/avenir-roman.eot");src:url("https://cdn.auth0.com/fonts/avenir/avenir-roman.eot?#iefix") format('embedded-opentype'),url("https://cdn.auth0.com/fonts/avenir/avenir-roman.woff") format('woff'),url("https://cdn.auth0.com/fonts/avenir/avenir-roman.ttf") format('truetype');font-weight:500;font-style:normal}@font-face{font-family:'avenir medium';src:url("https://cdn.auth0.com/fonts/avenir/avenir-medium.eot");src:url("https://cdn.auth0.com/fonts/avenir/avenir-medium.eot?#iefix") format('embedded-opentype'),url("https://cdn.auth0.com/fonts/avenir/avenir-medium.woff") format('woff'),url("https://cdn.auth0.com/fonts/avenir/avenir-medium.ttf") format('truetype');font-weight:500;font-style:normal}@font-face{font-family:'avenir book';src:url("https://cdn.auth0.com/fonts/avenir/avenir-book.eot");src:url("https://cdn.auth0.com/fonts/avenir/avenir-book.eot?#iefix") format('embedded-opentype'),url("https://cdn.auth0.com/fonts/avenir/avenir-book.woff") format('woff'),url("https://cdn.auth0.com/fonts/avenir/avenir-book.ttf") format('truetype');font-weight:normal;font-style:normal}@font-face{font-family:'ProximaNova Regular';src:url("https://cdn.auth0.com/fonts/proxima-nova/proximanova-regular-webfont-webfont.eot");src:url("https://cdn.auth0.com/fonts/proxima-nova/proximanova-regular-webfont-webfont.eot?#iefix") format('embedded-opentype'),url("https://cdn.auth0.com/fonts/proxima-nova/proximanova-regular-webfont-webfont.woff") format('woff');font-weight:normal;font-style:normal}@font-face{font-family:"budicon-font";src:url("https://cdn.auth0.com/fonts/budicons/fonts/budicon-font.eot");src:url("https://cdn.auth0.com/fonts/budicons/fonts/budicon-font.eot?#iefix") format("embedded-opentype"),url("https://cdn.auth0.com/fonts/budicons/fonts/budicon-font.woff") format("woff"),url("https://cdn.auth0.com/fonts/budicons/fonts/budicon-font.ttf") format("truetype"),url("https://cdn.auth0.com/fonts/budicons/fonts/budicon-font.svg#budicon-font") format("svg");font-weight:normal;font-style:normal}html,body{height:100%;margin:0;padding:0;font-size:62.5%;min-height:100%;width:100%;background-color:#222228;z-index:1}.modal-wrapper{width:100%;height:100%;display:table;background-color:rgba(0,0,0,0.15);z-index:2;-webkit-animation:fadein 1s;-moz-animation:fadein 1s;-ms-animation:fadein 1s;-o-animation:fadein 1s;animation:fadein 1s}.modal-centrix{padding:0;vertical-align:middle;display:table-cell;margin:0}.modal{width:100%;max-width:400px;z-index:3;border-radius:0;box-shadow:0 2px 4px rgba(0,0,0,0.5);margin:auto;}@media (min-width:414px){.modal{border-radius:5px;top:calc(50% + 40px)}}.modal .head{background:#efefef;background:-moz-linear-gradient(left,#efefef 0%,#fefefe 50%,#efefef 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0%,#efefef),color-stop(50%,#fefefe),color-stop(100%,#efefef));background:-webkit-linear-gradient(left,#efefef 0%,#fefefe 50%,#efefef 100%);background:-o-linear-gradient(left,#efefef 0%,#fefefe 50%,#efefef 100%);background:-ms-linear-gradient(left,#efefef 0%,#fefefe 50%,#efefef 100%);background:linear-gradient(to right,#efefef 0%,#fefefe 50%,#efefef 100%);text-align:center;height:132px;}@media (min-width:414px){.modal .head{border-radius:5px 5px 0 0}}.modal .head .logo{display:inline-block;margin:14px auto 0 auto;width:53px}.modal .head .first-line{display:block;line-height:30px;height:30px;margin:15px 0 0 0;font-family:'Avenir Roman';font-weight:normal;font-size:2.2rem;color:#000;}@media (min-width:414px){.modal .head .first-line{margin:9px 0 0 0}}.modal .head .second-line{display:block;line-height:16px;height:16px;margin:3px 0 21px 0;font-family:'Avenir Medium';font-weight:normal;font-size:1.2rem;color:#000;text-transform:uppercase}.modal .errors{text-align:center;background-color:#f04848;color:#fff;line-height:2;font-size:12px;font-family:'Avenir';font-weight:normal;padding:6px 20px}.modal .body{background-color:#fff;padding:30px 40px;overflow:hidden;}.modal .body .description{display:block;max-width:290px;margin:10px auto;font-family:'Avenir Roman';font-weight:normal;font-size:1.8rem;line-height:32px;color:rgba(0,0,0,0.8);text-align:center;font-family:'Avenir Book';font-weight:normal;font-size:1.6rem;line-height:24px;}@media (min-width:414px){.modal .body .description{max-width:348px}}.modal .body .description span{color:rgba(0,0,0,0.8)}.modal .body input[type=text]{border:none;border-bottom:1px solid #eee;height:21px;margin-right:2px;font-family:'Avenir Book';font-weight:normal;font-size:1.6rem;outline:none;width:100px}.modal .ok-cancel{display:block;width:100%;overflow:hidden;}.modal .ok-cancel button{height:75px;vertical-align:middle;width:50%;float:left;border:0;padding:5px 0 0 0;text-align:center;cursor:pointer;}.modal .ok-cancel button .icon{color:#fff;font-size:35px}.modal .ok-cancel button.ok{background-color:#40df7d;}.modal .ok-cancel button.ok:hover{background-color:#1dac54}.modal .ok-cancel button.ok.full-width{width:100%}@media (min-width:414px){.modal .ok-cancel button.ok{border-radius:0 0 5px 0}.modal .ok-cancel button.ok.full-width{border-radius:0 0 5px 5px}}.modal .ok-cancel button.cancel{background-color:#5c666f;}@media (min-width:414px){.modal .ok-cancel button.cancel{border-radius:0 0 0 5px}}[data-icon]:before{font-family:"budicon-font" !important;content:attr(data-icon);font-style:normal !important;font-weight:normal !important;font-variant:normal !important;text-transform:none !important;speak:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}[class^="icon-"]:before,[class*=" icon-"]:before{font-family:"budicon-font" !important;font-style:normal !important;font-weight:normal !important;font-variant:normal !important;text-transform:none !important;speak:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-budicon-377:before{content:""}.icon-budicon-509:before{content:""}.icon-budicon-460:before{content:""}.custom-select{display:inline-block;vertical-align:top;position:relative;border-radius:3px;height:32px;line-height:32px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:1.6rem;padding-right:28px;}.custom-select:hover{color:#333}.custom-select i{font-size:12px;position:absolute;top:3px;right:9px;opacity:.7;animation:none}.custom-select span{color:#000;border-bottom:1px solid #6b6b6b;padding-bottom:1px;text-transform:uppercase}.custom-select select{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0}@-moz-keyframes fadein{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadein{from{opacity:0}to{opacity:1}}@-ms-keyframes fadein{from{opacity:0}to{opacity:1}}@-o-keyframes fadein{from{opacity:0}to{opacity:1}}@-moz-keyframes fadein{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadein{from{opacity:0}to{opacity:1}}@-o-keyframes fadein{from{opacity:0}to{opacity:1}}@keyframes fadein{from{opacity:0}to{opacity:1}}</style>
</head>
<body>
<div class="modal-wrapper">
<div class="modal-centrix">
<div class="modal">
<form action="" method="POST" enctype="application/x-www-form-urlencoded">
<div class="head"><img src="https://cdn.auth0.com/styleguide/2.0.9/lib/logos/img/badge.png" class="logo auth0"><span class="first-line">Yubikey 2FA</span></div>
<div class="errors">
<% errors.forEach(function(error){ %>
<div class="p"><%= error %></div>
<%})%>
</div>
<div class="body"><span class="description">Hi <strong><%- user.name || user.email %></strong>, please tap your Yubikey.</span><span class="description domain"><span>Yubikey OTP:</span>
<input type="text" autocomplete="off" name="otp" required autofocus id="otp"></span></div>
<div class="ok-cancel">
<button class="ok full-width">
<div class="icon icon-budicon-509"></div>
</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment