diff --git a/accounts.js b/accounts.js index 3fa7815..ed9799a 100644 --- a/accounts.js +++ b/accounts.js @@ -62,18 +62,20 @@ function validateOtp(codeStore, codeId, token) { } function getOrCreate(store, username) { - return store.IssuerOauth3OrgAccounts.get(username).then(function (account) { - if (account) { - return account; + // account => profile + return store.IssuerOauth3OrgAccounts.get(username).then(function (profile) { + if (profile) { + return profile; } - account = { + // TODO profile should be ecdsa256 pub/privkeypair + profile = { username: username, accountId: makeB64UrlSafe(crypto.randomBytes(32).toString('base64')), }; - return store.IssuerOauth3OrgAccounts.create(username, account).then(function () { + return store.IssuerOauth3OrgAccounts.create(username, profile).then(function () { // TODO: put sort sort of email notification to the server managers? - return account; + return profile; }); }); } @@ -168,12 +170,13 @@ function createOtp(store, params) { }); } -function create(app) { +function create(deps, app) { var restful = {}; restful.sendOtp = function (req, res) { var params = req.body; var promise = req.getSiteStore().then(function (store) { + store.IssuerOauth3OrgProfiles = store.IssuerOauth3OrgProfiles || store.IssuerOauth3OrgAccounts; return createOtp(store, params).then(function (code) { var emailParams = { to: params.username, @@ -202,8 +205,13 @@ function create(app) { app.handlePromise(req, res, promise, '[issuer@oauth3.org] send one-time-password'); }; + // This should exchange: + // * 3rd party PPIDs for 1st Party Profile + // * Opaque Tokens for PPID Tokens restful.exchangeToken = function (req, res) { var OAUTH3 = require('./oauth3.js'); + var store = req.Models; + console.log('[exchangeToken] req.oauth3:'); console.log(req.oauth3); // req.oauth3.encodedToken @@ -218,47 +226,133 @@ function create(app) { credentialId: decoded.payload.sub + '@' + decoded.payload.iss //, sub: decoded.payload.sub //, iss: decoded.payload.iss - }).then(function (results) { + }).then(function (joins) { console.log('[exchangeToken] credentials profiles:'); - console.log(results); + console.log(joins); - if (!results) { - return { tokens: [] }; + function getToken(token) { + var tokenInfo = { + iat: Math.round(Date.now() / 1000) + , sub: token.sub + , iss: token.iss + , azp: token.iss + , aud: token.iss + }; + return restful.createToken._helper(req, res, tokenInfo); } - results = results.filter(function (el) { + function createProfile(credential, profile) { + var bs58 = require('bs58'); + var EC = require('elliptic').ec; + var ec = new EC('secp256k1'); + // TODO should be able to generate a private key without a library + // https://crypto.stackexchange.com/a/30273/53868 + var key = ec.genKeyPair(); + //var ec = new EC('curve25519'); + var pub = bs58.encode(Buffer.from(key.derive(key.getPublic()).toString('hex'), 'hex')); + var priv = bs58.encode(Buffer.from(key.priv.toString('hex'), 'hex')); + var parts = []; + + if (!credential.sub) { + return deps.Promise.reject(new Error("missing 'sub' from credential")); + } + + if (credential.sub !== profile.sub || credential.iss !== profile.iss) { + return deps.Promise.reject(new Error("credential 'sub' and 'iss' do not match information in profile")); + } + + parts.push(pub); + if (req.experienceId) { + parts.push(req.experienceId); + } + + var username = parts.join('@'); + var profile = { + accountId: pub // profile.sub + , sub: pub // profile.sub + , iss: req.experienceId + , prv: priv + , typ: 'oauth3' + }; + + function getId(token) { + var id = token.sub; + if (token.iss) { + id += '@' + token.iss; + } + return id || token.accountId; + } + + console.log('[debug] username, credential, profile:'); + console.log(username); + console.log(credential); + console.log(profile); + + return deps.Promise.reject(new Error("blah blah grrr grrr")); + + return store.IssuerOauth3OrgAccounts.create(username, profile).then(function () { + var id = crypto.randomBytes(16).toString('hex'); + return store.IssuerOauth3OrgCredentialsProfiles.create(id, { + credentialId: getId(credential) + , profileId: getId(profile) + }).then(function () { + // TODO: put sort sort of email notification to the server managers? + return getToken(profile).then(function (token) { + return { + tokens: [ token ] + //, error: { code: "E_NO_IMPL", message: "not implemented [177]" } + }; + }); + }); + }); + } + + function getProfile() { + var query = { username: 'IN ' + joins.map(function (el) { return el.profileId }).join(',') }; + //var query = { accountId: 'IN ' + joins.map(function (el) { return el.profileId }).join(',') }; + //var query = { accountId: joins.map(function (el) { return el.profileId })[0] }; + console.log('[DEBUG] query profiles:'); + console.log(query); + return req.Models.IssuerOauth3OrgAccounts.find(query).then(function (profiles) { + console.log('[DEBUG] Profiles:'); + console.log(profiles); + + profiles = (profiles||[]).filter(function (el) { + return !el.revokedAt && !el.deletedAt; + }); + + if (!profiles.length) { + return { tokens: [] }; + } + + return deps.Promise.all(profiles.map(function (profile) { + return getToken(profile); + })).then(function (tokens) { + return { + tokens: tokens + //, error: { code: "E_NO_IMPL", message: "not implemented [172]" } + }; + }); + }); + } + + joins = (joins||[]).filter(function (el) { return !el.revokedAt && !el.deletedAt; }); - if (!results.length) { - return { tokens: [] }; + if (joins.length) { + console.log('[DEBUG] CredentialsProfiles:'); + console.log(joins); + return getProfile(); } - return req.Models.IssuerOauth3OrgAccounts.find({ - accountId: 'IN ' + results.map(function (el) { return el.credentialId }).join(',') - }).then(function (profiles) { - if (!results) { - return { tokens: [] }; - } - - profiles = profiles.filter(function (el) { - return !el.revokedAt && !el.deletedAt; - }); - - if (!results.length) { - return { tokens: [] }; - } - - return req.deps.Promise.all(profiles.map(function (profile) { - var tokenInfo = { sub: profile.sub, iss: profile.iss, azp: profile.iss, aud: profile.iss }; - return restful.createToken._helper(req, res, tokenInfo); - })).then(function (tokens) { - return { - error: { code: "E_NO_IMPL", message: "not implemented [172]" } - , tokens: tokens - }; - }); - }); + if (!req.body || !req.body.create) { + console.log('[DEBUG] will not create'); + return { tokens: [] }; + } else { + console.log('[DEBUG] will create profile'); + return createProfile(req.oauth3.token, req.body); + } }); }); @@ -269,6 +363,7 @@ function create(app) { var store; var promise = req.getSiteStore().then(function (_store) { store = _store; + store.IssuerOauth3OrgProfiles = store.IssuerOauth3OrgProfiles || store.IssuerOauth3OrgAccounts; if (!req.body || !req.body.grant_type) { throw new OpErr("missing 'grant_type' from the body"); } @@ -294,7 +389,7 @@ function create(app) { app.handlePromise(req, res, promise, '[issuer@oauth3.org] create tokens'); }; restful.createToken._helper = function (req, res, token_info) { - return req.deps.Promise.resolve().then(function () { + return deps.Promise.resolve().then(function () { token_info.iss = req.experienceId; if (!token_info.aud) { throw new OpErr("missing required token field 'aud'"); @@ -318,7 +413,7 @@ function create(app) { } }); - return store.IssuerOauth3OrgGrants.find(search).then(function (grants) { + return req.Models.IssuerOauth3OrgGrants.find(search).then(function (grants) { if (!grants.length) { throw new OpErr("'"+token_info.azp+"' not given any grants from '"+(token_info.sub || token_info.azpSub)+"'"); } @@ -332,7 +427,7 @@ function create(app) { return token_info; }); }).then(function (token_info) { - return getPrivKey(store, req.experienceId).then(function (jwk) { + return getPrivKey(req.Models, req.experienceId).then(function (jwk) { var pem = require('jwk-to-pem')(jwk, { private: true }); var payload = { // standard @@ -391,6 +486,7 @@ function create(app) { var codeId = crypto.createHash('sha256').update(params.username_type+':'+params.username).digest('base64'); codeId = makeB64UrlSafe(codeId); return req.getSiteStore().then(function (store) { + store.IssuerOauth3OrgProfiles = store.IssuerOauth3OrgProfiles || store.IssuerOauth3OrgAccounts; return validateOtp(store.IssuerOauth3OrgCodes, codeId, params.password) .then(function () { return getOrCreate(store, params.username); @@ -406,7 +502,7 @@ function create(app) { console.log(contactClaim); return req.Models.IssuerOauth3OrgContactNodes.upsert(contactClaim).then(function () { return { - sub: account.accountId, + sub: account.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, }; diff --git a/models.js b/models.js index d9298ef..2468155 100644 --- a/models.js +++ b/models.js @@ -29,7 +29,8 @@ module.exports = [ { // TODO rename to profiles tablename: apiname + '_accounts', idname: 'username', - indices: baseFields.concat([ 'accountId' ]), + // make sub an ecdsa256 key + indices: baseFields.concat([ 'accountId', 'sub', 'iss', 'typ', 'privateKey' ]), // comment, recoveryNode }, { tablename: apiname + '_contact_nodes', diff --git a/package.json b/package.json index 103e901..1440700 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "bluebird": "^3.5.0", + "bs58": "^4.0.1", "elliptic": "^6.4.0", "jsonwebtoken": "^7.4.1", "jwk-to-pem": "^1.2.6", diff --git a/rest.js b/rest.js index c2d9e56..247fe75 100644 --- a/rest.js +++ b/rest.js @@ -3,7 +3,7 @@ module.exports.create = function (bigconf, deps, app) { var Jwks = require('./jwks').create(app); var Grants = require('./grants').create(app); - var Accounts = require('./accounts').create(app); + var Accounts = require('./accounts').create(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.