Support signing with another device
This commit is contained in:
parent
2ae4fec812
commit
44fcf30df8
41
persistence/models/challenge.js
Normal file
41
persistence/models/challenge.js
Normal file
@ -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;
|
||||||
|
};
|
@ -51,6 +51,7 @@ module.exports = function (options) {
|
|||||||
// TODO: Support multiple login options...
|
// TODO: Support multiple login options...
|
||||||
const loginMessage = {
|
const loginMessage = {
|
||||||
action: "authenticate",
|
action: "authenticate",
|
||||||
|
responseEndpoint: `http://${config.get("server.domain")}/xpub-auth/sign`,
|
||||||
url: config.get("server.domain"),
|
url: config.get("server.domain"),
|
||||||
userIdentifier: user.displayName,
|
userIdentifier: user.displayName,
|
||||||
}
|
}
|
||||||
@ -58,6 +59,17 @@ module.exports = function (options) {
|
|||||||
user.extendedPublicKeys[0].xpub,
|
user.extendedPublicKeys[0].xpub,
|
||||||
JSON.stringify(loginMessage) // TODO: Create a login body...
|
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) {
|
QRCode.toDataURL(JSON.stringify(challenge), function (err, url) {
|
||||||
if(err) {
|
if(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -69,15 +81,7 @@ module.exports = function (options) {
|
|||||||
user: request.user
|
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 => {
|
}).catch(error => {
|
||||||
console.error("Failed to fulfill account/authenticate post", request.body);
|
console.error("Failed to fulfill account/authenticate post", request.body);
|
||||||
console.error("Reason: ", error);
|
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')
|
router.route('/authenticate/response')
|
||||||
.post(function(request, response, next) {
|
.post(function(request, response, next) {
|
||||||
if(request.user) {
|
if(request.user) {
|
||||||
@ -168,28 +233,17 @@ module.exports = function (options) {
|
|||||||
// Challenge
|
// Challenge
|
||||||
const registerationMessage = {
|
const registerationMessage = {
|
||||||
action: "register",
|
action: "register",
|
||||||
|
responseEndpoint: `http://${config.get("server.domain")}/xpub-auth/sign`,
|
||||||
|
serviceExtendedPublicKey: config.get("bip32.serviceAuthenticatingExtendedPublicKey"),
|
||||||
url: config.get("server.domain"),
|
url: config.get("server.domain"),
|
||||||
userIdentifier: xpubUser.displayName, // identifier the user supplies on the login page
|
userIdentifier: xpubUser.displayName, // identifier the user supplies on the login page
|
||||||
user: xpubUser,
|
user: xpubUser
|
||||||
serviceExtendedPublicKey: config.get("bip32.serviceAuthenticatingExtendedPublicKey")
|
|
||||||
}
|
}
|
||||||
const challenge = hdAuthUtil.createChallenge(
|
const challenge = hdAuthUtil.createChallenge(
|
||||||
request.body.xpub, // update this to identifier...
|
request.body.xpub, // update this to identifier...
|
||||||
JSON.stringify(registerationMessage)
|
JSON.stringify(registerationMessage)
|
||||||
)
|
)
|
||||||
QRCode.toDataURL(JSON.stringify(challenge), function (err, url) {
|
return db.Challenge.create(challenge);
|
||||||
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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Empty User")
|
console.log("Empty User")
|
||||||
|
|
||||||
@ -197,6 +251,28 @@ module.exports = function (options) {
|
|||||||
response.render("error", {
|
response.render("error", {
|
||||||
user: request.user,
|
user: request.user,
|
||||||
message: "VOID"
|
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 => {
|
}).catch(error => {
|
||||||
@ -248,12 +324,74 @@ module.exports = function (options) {
|
|||||||
// user failed challenge
|
// user failed challenge
|
||||||
// TODO: Validate input
|
// TODO: Validate input
|
||||||
// TODO: Createa new challenge
|
// TODO: Createa new challenge
|
||||||
|
response.redirect("/register");
|
||||||
console.error("User failed to authenticate");
|
console.error("User failed to authenticate");
|
||||||
// Create new challenge and try again...
|
// 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')
|
router.route('/logout')
|
||||||
.post(function(request, response, next) {
|
.post(function(request, response, next) {
|
||||||
if(request.user) {
|
if(request.user) {
|
||||||
|
@ -21,8 +21,10 @@ module.exports = function (options) {
|
|||||||
|
|
||||||
// TODO: load child routers automatically
|
// TODO: load child routers automatically
|
||||||
var accountRouter = require('./account/index.js')(options);
|
var accountRouter = require('./account/index.js')(options);
|
||||||
|
var xpubAuth = require('./xpub-auth/index.js')(options);
|
||||||
|
|
||||||
router.use('/account', accountRouter);
|
router.use('/account', accountRouter);
|
||||||
|
router.use('/xpub-auth', xpubAuth);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
50
server/router/xpub-auth/index.js
Normal file
50
server/router/xpub-auth/index.js
Normal file
@ -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;
|
||||||
|
};
|
@ -41,7 +41,7 @@ block content
|
|||||||
|
|
||||||
a.btn-flat.copy-to-clipboard(data-clipboard-text=challenge) copy challenge to clipboard
|
a.btn-flat.copy-to-clipboard(data-clipboard-text=challenge) copy challenge to clipboard
|
||||||
form(action="/account/register/xpub/response" method="POST")
|
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="xpub", value=challenge.xpub)
|
||||||
input(type="hidden", name="message", value=challenge.message)
|
input(type="hidden", name="message", value=challenge.message)
|
||||||
input(type="hidden", name="derivationPath", value=challenge.derivationPath)
|
input(type="hidden", name="derivationPath", value=challenge.derivationPath)
|
||||||
|
@ -39,8 +39,16 @@ block content
|
|||||||
br
|
br
|
||||||
span(style="word-wrap:anywhere")= challenge.xpub
|
span(style="word-wrap:anywhere")= challenge.xpub
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col.s12
|
||||||
a.btn-flat.copy-to-clipboard(data-clipboard-text=challenge) copy challenge to clipboard
|
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")
|
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="xpub", value=challenge.xpub)
|
||||||
input(type="hidden", name="message", value=challenge.message)
|
input(type="hidden", name="message", value=challenge.message)
|
||||||
input(type="hidden", name="derivationPath", value=challenge.derivationPath)
|
input(type="hidden", name="derivationPath", value=challenge.derivationPath)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user