From 44fcf30df8d9fdafcbbff543c6f2ff586c3a5551 Mon Sep 17 00:00:00 2001 From: Kgothatso Date: Wed, 4 Dec 2019 21:46:42 +0200 Subject: [PATCH] Support signing with another device --- persistence/models/challenge.js | 41 ++++ server/router/account/index.js | 184 +++++++++++++++--- server/router/index.js | 2 + server/router/xpub-auth/index.js | 50 +++++ .../xpub-account-registeration-challenge.pug | 2 +- server/views/xpub-login.pug | 10 +- 6 files changed, 264 insertions(+), 25 deletions(-) create mode 100644 persistence/models/challenge.js create mode 100644 server/router/xpub-auth/index.js diff --git a/persistence/models/challenge.js b/persistence/models/challenge.js new file mode 100644 index 0000000..ba2cc05 --- /dev/null +++ b/persistence/models/challenge.js @@ -0,0 +1,41 @@ +const bcrypt = require('bcrypt'); + +module.exports = function (sequelize, DataTypes, options) { + const model = sequelize.define("challenge", { + id: { + type: DataTypes.STRING, + defaultValue: function() { + return options.generateUniqueId() + }, + primaryKey: true, + unique: true + }, + message: { + type: DataTypes.STRING, + allowNull: false + }, + derivationPath: { + type: DataTypes.STRING, + allowNull: false + }, + xpub: { + type: DataTypes.STRING, + allowNull: false + }, + request: { + type: DataTypes.JSON, + allowNull: false + }, + response: { + type: DataTypes.JSON + } + }, { + comment: "Challenge to be signed" + }); + + model.associate = (db) => { + + } + + return model; +}; \ No newline at end of file diff --git a/server/router/account/index.js b/server/router/account/index.js index d03b83c..6b9fd83 100644 --- a/server/router/account/index.js +++ b/server/router/account/index.js @@ -51,6 +51,7 @@ module.exports = function (options) { // TODO: Support multiple login options... const loginMessage = { action: "authenticate", + responseEndpoint: `http://${config.get("server.domain")}/xpub-auth/sign`, url: config.get("server.domain"), userIdentifier: user.displayName, } @@ -58,6 +59,17 @@ module.exports = function (options) { user.extendedPublicKeys[0].xpub, JSON.stringify(loginMessage) // TODO: Create a login body... ) + return db.Challenge.create(challenge); + } else { + // User doesn't exist register account + response.render("login-signup", { + user: request.user, + displayName: request.body.displayName, + pageTitle: "HD Auth - Signup", + }); + } + }).then(challenge => { + if(challenge) { QRCode.toDataURL(JSON.stringify(challenge), function (err, url) { if(err) { console.error(err); @@ -69,15 +81,7 @@ module.exports = function (options) { user: request.user }) }) - } else { - // User doesn't exist register account - response.render("login-signup", { - user: request.user, - displayName: request.body.displayName, - pageTitle: "HD Auth - Signup", - }); } - }).catch(error => { console.error("Failed to fulfill account/authenticate post", request.body); console.error("Reason: ", error); @@ -89,6 +93,67 @@ module.exports = function (options) { } }); + router.route('/authenticate/signed') + .post(function(request, response, next) { + if(request.user) { + response.redirect('/account'); + } else { + // Verify challenge + db.Challenge.findByPk(request.body.id, { + where: { + response: { + [db.Sequelize.Op.ne]: null + } + } + }).then(challenge => { + if (challenge) { + if(hdAuthUtil.verifyHDAuthChallengeResponse(challenge)) { + // user passed challenge... + var loginMessage = JSON.parse(challenge.message); + db.User.findOne({ + where: { + displayName: loginMessage.userIdentifier, + }, + include: [ + { + association: db.User.ExtendedPublicKeys + } + ] + }).then(user => { + if(user) { + // User created we can authenticate them on the site... + request.logIn(user, function(err) { + if (err) { return next(err); } + challenge.destroy() + .then(() => { + console.log("Deleted Challege: ", request.body.id); + }) + return response.redirect('/'); + }); + + } else { + console.error("Authenticated user doesn't exist: ", loginMessage); + } + }).catch(error => { + console.error("Failed to create authenticated user"); + console.error("Error: ", error); + next(error); + }) + } else { + // user failed challenge + // TODO: Validate input + // TODO: Createa new challenge + response.redirect("/register"); + console.error("User failed to authenticate"); + // Create new challenge and try again... + } + } else { + console.error("User challenge not signed: ", request.body); + } + }); + } + }) + router.route('/authenticate/response') .post(function(request, response, next) { if(request.user) { @@ -168,28 +233,17 @@ module.exports = function (options) { // Challenge const registerationMessage = { action: "register", + responseEndpoint: `http://${config.get("server.domain")}/xpub-auth/sign`, + serviceExtendedPublicKey: config.get("bip32.serviceAuthenticatingExtendedPublicKey"), url: config.get("server.domain"), userIdentifier: xpubUser.displayName, // identifier the user supplies on the login page - user: xpubUser, - serviceExtendedPublicKey: config.get("bip32.serviceAuthenticatingExtendedPublicKey") + user: xpubUser } const challenge = hdAuthUtil.createChallenge( request.body.xpub, // update this to identifier... JSON.stringify(registerationMessage) ) - QRCode.toDataURL(JSON.stringify(challenge), function (err, url) { - if(err) { - console.error(err); - } - response.render("xpub-account-registeration-challenge", { - xpubUser: xpubUser, - // challenge user to sign thier xpub - challenge: challenge, - qrCode: url, - user: request.user - }) - }) - + return db.Challenge.create(challenge); } else { console.log("Empty User") @@ -197,6 +251,28 @@ module.exports = function (options) { response.render("error", { user: request.user, message: "VOID" + }); + return null; + } + }).then(challenge => { + if(challenge) { + // send user a challenge... + QRCode.toDataURL(JSON.stringify(challenge), function (err, url) { + if(err) { + console.error(err); + } + response.render("xpub-account-registeration-challenge", { + // challenge user to sign thier xpub + challenge: challenge, + qrCode: url, + user: request.user + }) + }) + } else { + console.error("Couldn't create new challenge"); + response.render("error", { + user: request.user, + message: "Failed to create challenge" }) } }).catch(error => { @@ -248,12 +324,74 @@ module.exports = function (options) { // user failed challenge // TODO: Validate input // TODO: Createa new challenge + response.redirect("/register"); console.error("User failed to authenticate"); // Create new challenge and try again... } } }) + router.route('/register/xpub/signed') + .post(function(request, response, next) { + if(request.user) { + response.redirect('/account'); + } else { + // Verify challenge + db.Challenge.findByPk(request.body.id, { + where: { + response: { + [db.Sequelize.Op.ne]: null + } + } + }).then(challenge => { + if (challenge) { + if(hdAuthUtil.verifyHDAuthChallengeResponse(challenge)) { + // user passed challenge... + // TODO: Load registration request from challenge.message + // TODO: build and validate that user owns xpub... + var xpubUser = JSON.parse(challenge.message).user; + // Possibility that username is taken... + // TODO: Create user without username + db.User.create(xpubUser, { + include: [ + { + association: db.User.ExtendedPublicKeys + } + ] + }).then(user => { + if(user) { + // User created we can authenticate them on the site... + request.logIn(user, function(err) { + if (err) { return next(err); } + challenge.destroy() + .then(() => { + console.log("Deleted Challege: ", request.body.id); + }) + return response.redirect('/'); + }); + // TODO: delete challenge... + } + + }).catch(error => { + console.error("Failed to create authenticated user"); + console.error("Error: ", error); + next(error); + }) + } else { + // user failed challenge + // TODO: Validate input + // TODO: Createa new challenge + response.redirect("/register"); + console.error("User failed to authenticate"); + // Create new challenge and try again... + } + } else { + console.error("User challenge not signed: ", request.body); + } + }); + } + }) + router.route('/logout') .post(function(request, response, next) { if(request.user) { diff --git a/server/router/index.js b/server/router/index.js index ebee3b1..9bdeaf1 100644 --- a/server/router/index.js +++ b/server/router/index.js @@ -21,8 +21,10 @@ module.exports = function (options) { // TODO: load child routers automatically var accountRouter = require('./account/index.js')(options); + var xpubAuth = require('./xpub-auth/index.js')(options); router.use('/account', accountRouter); + router.use('/xpub-auth', xpubAuth); return router; }; \ No newline at end of file diff --git a/server/router/xpub-auth/index.js b/server/router/xpub-auth/index.js new file mode 100644 index 0000000..ab52441 --- /dev/null +++ b/server/router/xpub-auth/index.js @@ -0,0 +1,50 @@ +/** + * This router handles things related to the web browser experience... + */ +// This is the mock data we working with... + +module.exports = function (options) { + const config = require('config'); + var QRCode = require('qrcode'); + var express = options.express; + + const db = options.db; + const hdAuthUtil = options.hdAuthUtil; + + var router = express.Router(); + + router.route('/sign') + .post(function(request, response, next) { + // Verify challenge + db.Challenge.findByPk(request.body.id, + { + where: { + response: null + } + }).then(challenge => { + if (challenge) { + challenge.response = { + signature: request.body.signature + }; + if(hdAuthUtil.verifyHDAuthChallengeResponse(challenge)) { + challenge.save().then(() => { + response.json({ + "message": "challenge response accepted" + }) + }); + } else { + response.status(401); + // user failed challenge + // TODO: Validate input + // TODO: Createa new challenge + console.error("User failed to authenticate"); + // Create new challenge and try again... + } + } else { + response.status(500); + } + }); + }) + // TODO: add other endpoints + return router; +}; \ No newline at end of file diff --git a/server/views/xpub-account-registeration-challenge.pug b/server/views/xpub-account-registeration-challenge.pug index ed2dc1a..311d7f1 100644 --- a/server/views/xpub-account-registeration-challenge.pug +++ b/server/views/xpub-account-registeration-challenge.pug @@ -41,7 +41,7 @@ block content a.btn-flat.copy-to-clipboard(data-clipboard-text=challenge) copy challenge to clipboard form(action="/account/register/xpub/response" method="POST") - input(type="hidden", name="displayName", value=xpubUser.displayName) + input(type="hidden", name="id", value=challenge.id) input(type="hidden", name="xpub", value=challenge.xpub) input(type="hidden", name="message", value=challenge.message) input(type="hidden", name="derivationPath", value=challenge.derivationPath) diff --git a/server/views/xpub-login.pug b/server/views/xpub-login.pug index 5be22fd..f226fbc 100644 --- a/server/views/xpub-login.pug +++ b/server/views/xpub-login.pug @@ -39,8 +39,16 @@ block content br span(style="word-wrap:anywhere")= challenge.xpub - a.btn-flat.copy-to-clipboard(data-clipboard-text=challenge) copy challenge to clipboard + .row + .col.s12 + a.btn-flat.copy-to-clipboard(data-clipboard-text=challenge) copy challenge to clipboard + .row + .col.s12 + form(action="/account/authenticate/signed", method="post") + input(type="hidden", name="id", value=challenge.id) + button.btn-flat.blue-text(type="submit") signed on seperate device form(action="/account/authenticate/response" method="POST") + input(type="hidden", name="id", value=challenge.id) input(type="hidden", name="xpub", value=challenge.xpub) input(type="hidden", name="message", value=challenge.message) input(type="hidden", name="derivationPath", value=challenge.derivationPath)