From d20e40203edb2f13e3b5ccd0ebfd098d58e2ab24 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Fri, 21 Jul 2017 10:45:01 -0600 Subject: [PATCH] implemented sending of one-time-passwords --- models.js | 8 +++--- rest.js | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/models.js b/models.js index 7dc3c54..6f5b59e 100644 --- a/models.js +++ b/models.js @@ -7,19 +7,21 @@ module.exports = [ { tablename: apiname + '_private_keys', idname: 'id', - unique: ['id'], indices: baseFields.concat([ 'kty', 'kid' ]), }, + { + tablename: apiname + '_codes', + idname: 'id', + indices: baseFields.concat([ 'code', 'expires' ]), + }, { tablename: apiname + '_jwks', idname: 'id', - unique: ['id'], indices: baseFields.concat([ 'kty', 'kid', 'sub' ]), }, { tablename: apiname + '_grants', idname: 'id', - unique: ['id'], indices: baseFields.concat([ 'sub', 'azp', 'azpSub', 'scope' ]), }, ]; diff --git a/rest.js b/rest.js index ece9586..3f2abc5 100644 --- a/rest.js +++ b/rest.js @@ -212,6 +212,22 @@ module.exports.create = function (bigconf, deps, app) { app.handlePromise(req, res, promise, '[issuer@oauth3.org] save grants'); }; + Tokens.retrieveOtpCode = function (codeStore, codeId) { + return codeStore.get(codeId).then(function (code) { + if (!code) { + return null; + } + + var expires = (new Date(code.expires)).valueOf(); + if (!expires || Date.now() > expires) { + return codeStore.destroy(codeId).then(function () { + return null; + }); + } + + return code; + }); + }; Tokens.getPrivKey = function (store, experienceId) { return store.IssuerOauth3OrgPrivateKeys.get(experienceId).then(function (jwk) { if (jwk) { @@ -235,6 +251,66 @@ module.exports.create = function (bigconf, deps, app) { }); }; + Tokens.restful.sendOtp = function (req, res) { + var params = req.body; + var promise = PromiseA.resolve().then(function () { + if (!params || !params.username) { + throw new Error("must provide the email address as 'username' in the body"); + } + if ((params.username_type && 'email' !== params.username_type) || !/@/.test(params.username)) { + throw new Error("only email one-time login codes are supported at this time"); + } + params.username_type = 'email'; + + return req.getSiteStore(); + }).then(function (store) { + var codeStore = store.IssuerOauth3OrgCodes; + var codeId = crypto.createHash('sha256').update(params.username_type+':'+params.username).digest('base64'); + codeId = makeB64UrlSafe(codeId); + + return Tokens.retrieveOtpCode(codeStore, codeId).then(function (code) { + if (code) { + return code; + } + + var token = ''; + while (!/^\d{4}-\d{4}-\d{4}$/.test(token)) { + // Most of the number we can generate this was start with 1 (and no matter what can't + // start with 0), so we don't use the very first digit. Also basically all of the + // numbers are too big to accurately store in JS floats, so we limit the trailing 0's. + token = (parseInt(crypto.randomBytes(8).toString('hex'), 16)).toString() + .replace(/0+$/, '0').replace(/\d(\d{4})(\d{4})(\d{4}).*/, '$1-$2-$3'); + } + code = { + id: codeId, + code: token, + expires: new Date(Date.now() + 20*60*1000), + }; + return codeStore.upsert(codeId, code).then(function (){ + return code; + }); + }); + }).then(function (code) { + var emailParams = { + to: params.username, + from: 'login@daplie.com', // opts.mailer.defaults.system + replyTo: 'hello@daplie.com', + subject: "Use " + code.code + " as your Login Code", // message.Subject + text: code.code + " is your Login Code." // message['stripped-html'] + }; + emailParams['h:Reply-To'] = emailParams.replyTo; + + return req.getSiteMailer().sendMailAsync(emailParams).then(function () { + return { + id: code.id, + expires: code.expires, + created: new Date(parseInt(code.createdAt, 10) || code.createdAt), + }; + }); + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] send one-time-password'); + }; Tokens.restful.create = function (req, res) { var store; var promise = req.getSiteStore().then(function (_store) { @@ -288,6 +364,7 @@ module.exports.create = function (bigconf, deps, app) { app.get( '/grants/:sub/:azp', Grants.restful.getOne); app.post( '/grants/:sub/:azp', Grants.restful.saveNew); + app.post( '/access_token/send_otp', Tokens.restful.sendOtp); app.use( '/access_token/:sub', authorizeIssuer); app.post( '/access_token/:sub/:aud/:azp', Tokens.restful.create);