From 03c5974a78f5fbc84cc0802d6aef31e0c2de40da Mon Sep 17 00:00:00 2001 From: aj Date: Tue, 12 Sep 2017 22:31:11 +0000 Subject: [PATCH] use db to retrieve subs --- accounts.js | 243 +++++++++++++++++++++++++++++++++++++++------------- common.js | 4 +- models.js | 5 ++ rest.js | 39 ++++++--- 4 files changed, 216 insertions(+), 75 deletions(-) diff --git a/accounts.js b/accounts.js index 7c8d966..232e482 100644 --- a/accounts.js +++ b/accounts.js @@ -21,6 +21,7 @@ function retrieveOtp(codeStore, codeId) { return code; }); } + function validateOtp(codeStore, codeId, token) { if (!codeId) { return PromiseA.reject(new Error("Must provide authcode ID")); @@ -127,68 +128,73 @@ function timespan(duration, max) { return duration; } +function createOtp(store, params) { + return PromiseA.resolve().then(function () { + if (!params || !params.username) { + throw new OpErr("must provide the email address as 'username' in the body"); + } + if ((params.username_type && 'email' !== params.username_type) || !/@/.test(params.username)) { + throw new OpErr("only email one-time login codes are supported at this time"); + } + params.username_type = 'email'; + + var codeStore = store.IssuerOauth3OrgCodes; + var codeId = crypto.createHash('sha256').update(params.username_type+':'+params.username).digest('base64'); + codeId = makeB64UrlSafe(codeId); + + return retrieveOtp(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), + node: { type: params.username_type, node: params.username } + }; + return codeStore.upsert(codeId, code).then(function (){ + return code; + }); + }); + }); +} + function create(app) { var restful = {}; restful.sendOtp = function (req, res) { var params = req.body; - var promise = PromiseA.resolve().then(function () { - if (!params || !params.username) { - throw new OpErr("must provide the email address as 'username' in the body"); - } - if ((params.username_type && 'email' !== params.username_type) || !/@/.test(params.username)) { - throw new OpErr("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 retrieveOtp(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), + var promise = req.getSiteStore().then(function (store) { + return createOtp(store, params).then(function (code) { + var emailParams = { + to: params.username, + from: 'login@daplie.com', + replyTo: 'hello@daplie.com', + subject: "Use " + code.code + " as your Login Code", + text: "Your login code is:\n\n" + + code.code + + "\n\nThis email address was used to request to add your Hello ID to a device." + + "\nIf you did not make the request you can safely ignore this message." }; - return codeStore.upsert(codeId, code).then(function (){ - return code; - }); - }); - }).then(function (code) { - var emailParams = { - to: params.username, - from: 'login@daplie.com', - replyTo: 'hello@daplie.com', - subject: "Use " + code.code + " as your Login Code", - text: "Your login code is:\n\n" - + code.code - + "\n\nThis email address was used to request to add your Hello ID to a device." - + "\nIf you did not make the request you can safely ignore this message." - }; - emailParams['h:Reply-To'] = emailParams.replyTo; + emailParams['h:Reply-To'] = emailParams.replyTo; - return req.getSiteCapability('email@daplie.com').then(function (mailer) { - return mailer.sendMailAsync(emailParams).then(function () { - return { - code_id: code.id, - expires: code.expires, - created: new Date(parseInt(code.createdAt, 10) || code.createdAt), - }; + return req.getSiteCapability('email@daplie.com').then(function (mailer) { + return mailer.sendMailAsync(emailParams).then(function () { + return { + code_id: code.id, + expires: code.expires, + created: new Date(parseInt(code.createdAt, 10) || code.createdAt), + }; + }); }); }); }); @@ -317,16 +323,28 @@ function create(app) { .then(function () { return getOrCreate(store, params.username); }).then(function (account) { - return { - sub: account.accountId, - aud: req.params.aud || req.body.aud || req.experienceId, - azp: req.params.azp || req.body.azp || req.body.client_id || req.body.client_uri || req.experienceId, - }; + var contactClaimId = crypto.createHash('sha256').update(account.accountId+':'+params.username_type+':'+params.username).digest('base64'); + return req.Models.IssuerOauth3OrgContactNodes.get(contactClaimId).then(function (contactClaim) { + var now = Date.now(); + if (!contactClaim) { contactClaim = { id: contactClaimId }; } + if (!contactClaim.verifiedAt) { contactClaim.verifiedAt = now; } + contactClaim.lastVerifiedAt = now; + + console.log('contactClaim'); + console.log(contactClaim); + return req.Models.IssuerOauth3OrgContactNodes.upsert(contactClaim).then(function () { + return { + sub: account.accountId, + aud: req.params.aud || req.body.aud || req.experienceId, + azp: req.params.azp || req.body.azp || req.body.client_id || req.body.client_uri || req.experienceId, + }; + }); + }); }); }); }; restful.createToken.issuerToken = function (req) { - return require('./common').checkIsserToken(req, req.params.sub || req.body.sub).then(function (sub) { + return require('./common').checkIssuerToken(req, req.params.sub || req.body.sub).then(function (sub) { return { sub: sub, aud: req.params.aud || req.body.aud || req.experienceId, @@ -353,6 +371,109 @@ function create(app) { }); }; + restful.getProfile = function (req, res) { + var promise = req.Models.IssuerOauth3OrgAccounts.get(req.oauth3.accountIdx).then(function (result) { + if (!result) { result = { id: undefined }; } + + result.id = undefined; + + return result; + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] get profile'); + }; + restful.setProfile = function (req, res) { + console.log('req.oauth3'); + console.log(req.oauth3); + + var body = req.body; + var promise = req.Models.IssuerOauth3OrgAccounts.find({ accountId: req.oauth3.ppid }).then(function (results) { + var result = results[0]; + var changed = false; + + console.log('get gotten'); + console.log(results); + + if (!result) { throw new OpErr("account could not be found"); /*result = { accountId: req.oauth3.accountIdx, displayName: '', firstName: '', lastName: '', avatarUrl: '' };*/ } + + // TODO schema for validation + [ 'firstName', 'lastName', 'avatarUrl', 'displayName' ].forEach(function (key) { + if ('string' === typeof body[key] && -1 === [ 'null', 'undefined' ].indexOf(body[key])) { + if (result[key] !== body[key]) { + result[key] = body[key]; + changed = true; + } + } + }); + + if (changed) { + return req.Models.IssuerOauth3OrgAccounts.upsert(result).then(function () { console.log('update updated'); return result; }); + } + + return result; + }).then(function (result) { + result.id = undefined; + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] set profile'); + }; + restful.listContactNodes = function (req, res) { + }; + restful.claimContact = function (req, res) { + var type = req.body.type; + var node = req.body.node; + var promise = createOtp(req.Models, { username_type: type, username: node }).then(function (code) { + var emailParams = { + to: node, + from: 'login@daplie.com', + replyTo: 'hello@daplie.com', + subject: "Verify your email address: " + code.code, + text: "Your verification code is:\n\n" + + code.code + + "\n\nThis email address was requested to be used with Hello ID." + + "\nIf you did not make the request you can safely ignore this message." + }; + emailParams['h:Reply-To'] = emailParams.replyTo; + + return req.getSiteCapability('email@daplie.com').then(function (mailer) { + return mailer.sendMailAsync(emailParams).then(function () { + return { + code_id: code.id, + expires: code.expires, + created: new Date(parseInt(code.createdAt, 10) || code.createdAt), + }; + }); + }); + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] claim contact'); + }; + restful.verifyContact = function (req, res) { + var codeId = req.params.id; + var challenge = req.body.challenge || req.body.token || req.body.code; + var store = req.Models; + + var promise = validateOtp(store.IssuerOauth3OrgCodes, codeId, challenge).then(function (code) { + if (!code.node || !code.node.type || !code.node.node) { + throw new OpErr("code didn't have contact node and type information"); + } + + var contactClaimId = crypto.createHash('sha256').update(req.oauth3.accountIdx+':'+code.node.type+':'+code.node.node).digest('base64'); + return req.Models.IssuerOauth3OrgContactNodes.get(contactClaimId).then(function (contactClaim) { + var now = Date.now(); + if (!contactClaim) { contactClaim = { id: contactClaimId }; } + if (!contactClaim.verifiedAt) { contactClaim.verifiedAt = now; } + contactClaim.lastVerifiedAt = now; + + return req.Models.IssuerOauth3OrgContactNodes.upsert(contactClaim).then(function () { + return { success: true }; + }); + }); + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] verify contact'); + }; + return { restful: restful, }; diff --git a/common.js b/common.js index 8769ca2..e95ea5b 100644 --- a/common.js +++ b/common.js @@ -7,7 +7,7 @@ function makeB64UrlSafe(b64) { return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); } -function checkIsserToken(req, expectedSub) { +function checkIssuerToken(req, expectedSub) { if (!req.oauth3 || !req.oauth3.verifyAsync) { return PromiseA.reject(new OpErr("request requires a token for authorization")); } @@ -40,5 +40,5 @@ function checkIsserToken(req, expectedSub) { }); } -module.exports.checkIsserToken = checkIsserToken; +module.exports.checkIssuerToken = checkIssuerToken; module.exports.makeB64UrlSafe = makeB64UrlSafe; diff --git a/models.js b/models.js index 2cdfee3..8e1e017 100644 --- a/models.js +++ b/models.js @@ -19,6 +19,11 @@ module.exports = [ idname: 'username', indices: baseFields.concat([ 'accountId' ]), }, + { + tablename: apiname + '_contact_nodes', + idname: 'id', + indices: baseFields.concat([ 'accountId', 'verifiedAt', 'lastVerifiedAt' ]), + }, { tablename: apiname + '_jwks', idname: 'id', diff --git a/rest.js b/rest.js index cad511e..f10532f 100644 --- a/rest.js +++ b/rest.js @@ -7,6 +7,12 @@ module.exports.create = function (bigconf, deps, app) { // This tablename is based on the tablename found in the objects in model.js. // Instead of the snake_case the name with be UpperCammelCase, converted by masterquest-sqlite3. + function attachSiteModels(req, res, next) { + return req.getSiteStore().then(function (store) { + req.Models = store; + next(); + }); + } function attachSiteStore(tablename, req, res, next) { return req.getSiteStore().then(function (store) { req.Store = store[tablename]; @@ -18,28 +24,37 @@ module.exports.create = function (bigconf, deps, app) { next(); } function authorizeIssuer(req, res, next) { - var promise = require('./common').checkIsserToken(req, req.params.sub).then(function () { + var promise = require('./common').checkIssuerToken(req, req.params.sub).then(function () { next(); }); app.handleRejection(req, res, promise, '[issuer@oauth3.org] authorize req as issuer'); } - app.get( '/jwks/:sub/:kid.json', Jwks.restful.get); - app.get( '/jwks/:sub/:kid', Jwks.restful.get); + app.get( '/jwks/:sub/:kid.json', Jwks.restful.get); + app.get( '/jwks/:sub/:kid', Jwks.restful.get); // Everything but getting keys is only for the issuer - app.use( '/jwks/:sub', authorizeIssuer, attachSiteStore.bind(null, 'IssuerOauth3OrgJwks')); - app.post( '/jwks/:sub', Jwks.restful.saveNew); + app.use( '/jwks/:sub', authorizeIssuer, attachSiteStore.bind(null, 'IssuerOauth3OrgJwks')); + app.post( '/jwks/:sub', Jwks.restful.saveNew); // Everything regarding grants is only for the issuer - app.use( '/grants/:sub', authorizeIssuer, attachSiteStore.bind(null, 'IssuerOauth3OrgGrants')); - app.get( '/grants/:sub', Grants.restful.getAll); - app.get( '/grants/:sub/:azp', Grants.restful.getOne); - app.post( '/grants/:sub/:azp', Grants.restful.saveNew); + app.use( '/grants/:sub', authorizeIssuer, attachSiteStore.bind(null, 'IssuerOauth3OrgGrants')); + app.get( '/grants/:sub', Grants.restful.getAll); + app.get( '/grants/:sub/:azp', Grants.restful.getOne); + app.post( '/grants/:sub/:azp', Grants.restful.saveNew); - app.post( '/access_token/send_otp', Accounts.restful.sendOtp); - app.post( '/access_token/:sub/:aud/:azp', Accounts.restful.createToken); - app.post( '/access_token', Accounts.restful.createToken); + app.use( '/access_token', attachSiteModels); + app.post( '/access_token/send_otp', Accounts.restful.sendOtp); + app.post( '/access_token/:sub/:aud/:azp', Accounts.restful.createToken); + app.post( '/access_token', Accounts.restful.createToken); + + app.use( '/acl/profile', attachSiteModels); + app.get( '/acl/profile', Accounts.restful.getProfile); + app.post( '/acl/profile', Accounts.restful.setProfile); + + app.use( '/acl/contact_nodes', attachSiteModels); + app.post( '/acl/contact_nodes', Accounts.restful.claimContact); + app.post( '/acl/contact_nodes/:id', Accounts.restful.verifyContact); app.use(detachSiteStore); };