diff --git a/accounts.js b/accounts.js index 4aa653c..699a16e 100644 --- a/accounts.js +++ b/accounts.js @@ -63,7 +63,7 @@ function validateOtp(codeStore, codeId, token) { function getOrCreate(store, iss, username) { // account => profile - return store.IssuerOauth3OrgProfiles.find({ username: username }).then(function (profile) { + return store.IssuerOauth3OrgProfiles.find({ username: username }).then(filterRejectable).then(function (profile) { profile = profile && profile[0]; if (profile) { return profile; @@ -77,6 +77,7 @@ function getOrCreate(store, iss, username) { , username: username , sub: sub , iss: iss + , typ: username ? 'username' : 'profile' }; return store.IssuerOauth3OrgProfiles.create(profile.id, profile).then(function () { // TODO: put sort sort of email notification to the server managers? @@ -210,156 +211,6 @@ function create(deps, 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 - - console.log('[exchangeToken] OAUTH3.jwk:'); - console.log(OAUTH3.jwk); - - var promise = OAUTH3.jwk.verifyToken(req.oauth3.encodedToken).then(function (decoded) { - console.log('[exchangeToken] verified token:'); - console.log(decoded); - // TODO handle opaque tokens by exchanging at issuer -- if (!token.sub && token.jti) { ... } - return req.Models.IssuerOauth3OrgCredentialsProfiles.find({ - credentialId: decoded.payload.sub + '@' + decoded.payload.iss - //, sub: decoded.payload.sub - //, iss: decoded.payload.iss - }).then(function (joins) { - console.log('[exchangeToken] credentials profiles:'); - console.log(joins); - - 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); - } - - function createProfile(credential, meta) { - 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 iss = req.experienceId; - var id = pub + '@' + iss; - - if (!credential.sub) { - return deps.Promise.reject(new Error("missing 'sub' from credential")); - } - - if (credential.sub !== meta.sub || credential.iss !== meta.iss) { - return deps.Promise.reject(new Error("credential 'sub' and 'iss' do not match information in request body")); - } - - var profile = { - id: id - // accountId: pub // profile.sub - , sub: pub // profile.sub - , iss: req.experienceId - , prv: priv - , typ: 'profile' - , username: id - }; - - function getId(token) { - var id = token.sub; - if (token.iss) { - id += '@' + token.iss; - } - return id || token.id || token.accountId; - } - - console.log('[debug] id, credential, profile:'); - console.log(id); - console.log(credential); - console.log(profile); - - return store.IssuerOauth3OrgProfiles.create(profile.id/*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 = { id: 'IN ' + joins.map(function (el) { return el.profileId }).join(',') }; - //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.IssuerOauth3OrgProfiles.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 (joins.length) { - console.log('[DEBUG] CredentialsProfiles:'); - console.log(joins); - return getProfile(); - } - - 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); - } - }); - }); - - app.handlePromise(req, res, promise, '[issuer@oauth3.org] exchangeToken'); - }; - restful.createToken = function (req, res) { var store; var promise = req.getSiteStore().then(function (_store) { @@ -379,7 +230,7 @@ function create(deps, app) { return restful.createToken.refreshToken(req); } if (req.body.grant_type === 'exchange_token') { - return restful.exchangeToken(req); + return restful.createToken.exchangeToken(req); } throw new OpErr("unknown or un-implemented grant_type '"+req.body.grant_type+"'"); @@ -389,6 +240,7 @@ function create(deps, app) { app.handlePromise(req, res, promise, '[issuer@oauth3.org] create tokens'); }; + restful.createToken._helper = function (req, res, token_info) { return deps.Promise.resolve().then(function () { token_info.iss = req.experienceId; @@ -414,7 +266,7 @@ function create(deps, app) { } }); - return req.Models.IssuerOauth3OrgGrants.find(search).then(function (grants) { + return req.Models.IssuerOauth3OrgGrants.find(search).then(filterRejectable).then(function (grants) { if (!grants.length) { throw new OpErr("'"+token_info.azp+"' not given any grants from '"+(token_info.sub || token_info.azpSub)+"'"); } @@ -471,6 +323,48 @@ function create(deps, app) { }); }); }; + + // This should exchange: + // * 3rd party PPIDs for 1st Party Profile + // * Opaque Tokens for PPID Tokens + restful.createToken.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 + + console.log('[exchangeToken] OAUTH3.jwk:'); + console.log(OAUTH3.jwk); + + var promise = OAUTH3.jwk.verifyToken(req.oauth3.encodedToken).then(function (completeDecoded) { + var p; + + console.log('[exchangeToken] verified token:'); + console.log(completeDecoded); + // TODO handle opaque tokens by exchanging at issuer -- if (!token.sub && token.jti) { ... } + + if (!req.body || !req.body.create) { + return Profiles.get(req, res, completeDecoded.payload).then(function (profiles) { + return deps.Promise.all(profiles.map(function (profile) { + return Profiles._getToken(req, res, profile); + })); + }).then(function (tokens) { + return { tokens: tokens }; + }); + } + + return Profiles.getOrCreate(req, res, completeDecoded.payload).then(function () { + return Profiles._getToken(req, res, profile).then(function (token) { + return { tokens: [ token ] }; + }); + }); + + }); + + app.handlePromise(req, res, promise, '[issuer@oauth3.org] exchangeToken'); + }; + restful.createToken.password = function (req) { var params = req.body; if (!params || !params.username) { @@ -541,14 +435,230 @@ function create(deps, app) { }); }; + function rejectDeleted(el) { + if (el && (!el.revokedAt && !el.deletedAt)) { + return el; + } + return null; + } + function filterRejectable(arr) { + return arr.filter(rejectDeleted); + } + + var Credentials = {}; + Credentials.getOrCreate = function (credential) { + var query = {}; + var id; + var result; + + if (credential.username) { + query.username = credential.username; + id = credential.username; + } else if (credential.iss) { + query.sub = credential.sub; + query.iss = credential.iss; + id = query.sub + '@' + query.iss; + } + + return req.Models.IssuerOauth3OrgCredentials.find(query).then(filterRejectable).then(function (_credentials) { + if (_credentials.length) { + return _credentials[0]; + } + + result = { + username: credential.username + , sub: credential.sub + , iss: credential.iss + , typ: username ? 'username' : 'profile' + }; + + return req.Models.IssuerOauth3OrgCredentials.create(id, result).then(function () { + result.id = id; + return result; + }); + }); + + }; + + var Profiles = {}; + Profiles.id = function (token) { + var id = token.sub || ''; + if (token.iss) { + id += '@' + token.iss; + } + return id || token.id || token.accountIdx || token.accountId; + }; + Profiles.create = function (req, res, credential, meta) { + meta = meta || {}; + var pub = meta.sub; + var store = req.Models; + var iss; + + if (!meta.sub) { + 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'); + pub = bs58.encode(Buffer.from(key.derive(key.getPublic()).toString('hex'), 'hex')); + var priv = bs58.encode(Buffer.from(key.priv.toString('hex'), 'hex')); + iss = req.experienceId; + } else { + iss = credential.iss; + } + + var id = pub + '@' + iss; + + if (!credential.sub) { + return deps.Promise.reject(new Error("missing 'sub' from credential")); + } + + var profile = { + id: id + // accountId: pub // profile.sub + , sub: pub // profile.sub + , iss: iss + , prv: priv + , typ: 'profile' + , username: id + }; + + console.log('[debug] id, credential, profile:'); + console.log(id); + console.log(credential); + console.log(profile); + + console.log('[Profiles.create] profile:'); + console.log(profile); + return store.IssuerOauth3OrgProfiles.create(profile.id/*username*/, profile).then(function () { + console.log('[Profiles.create] created!'); + var id = crypto.randomBytes(16).toString('hex'); + return store.IssuerOauth3OrgCredentialsProfiles.create(id, { + credentialId: Profiles.id(credential) + , profileId: Profiles.id(profile) + }).then(function () { + // TODO: put sort sort of email notification to the server managers? + return profile; + }); + }); + }; + Profiles._getToken = function (req, res, 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); + }; + Profiles.ids = function (req, res, decoded) { + return req.Models.IssuerOauth3OrgCredentialsProfiles.find({ + credentialId: decoded.sub + '@' + decoded.iss + //, sub: decoded.payload.sub + //, iss: decoded.payload.iss + }).then(filterRejectable).then(function (joins) { + console.log('[exchangeToken] credentials profiles:'); + console.log(joins); + + return joins; + }); + }; + Profiles.get = function (req, res, decoded) { + return Profiles.ids(decoded).then(function (joins) { + return Profiles.getFromIds(req, res, joins); + }); + }; + Profiles.getFromIds = function (req, res, joins) { + var query = { id: 'IN ' + joins.map(function (el) { return el.profileId }).join(',') }; + //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 Profiles._get(req, res, query).then(function (profiles) { + console.log('[DEBUG] Profiles:'); + console.log(profiles); + + return profiles; + }); + }; + Profiles._get = function (req, res, query) { + return req.Models.IssuerOauth3OrgProfiles.find(query).then(filterRejectable); + }; + Profiles._one = function (req, res, id) { + console.log('[Profiles._one] id:', id); + return req.Models.IssuerOauth3OrgProfiles.get(id).then(function (p) { + console.log('[Profiles._one] p:', p); + return p; + }).then(rejectDeleted); + }; + Profiles.oneOrCreate = function (req, res, cred) { + var sub; + var iss; + var tok; + if (cred.accountIdx) { + sub = cred.accountIdx.split('@')[0]; + iss = cred.accountIdx.split('@')[1]; + } + tok = { sub: sub || cred.sub, iss: iss || cred.iss }; + + var id = Profiles.id(cred); + console.log('[oneOrCreate] id:', id); + return Profiles._one(req, res, id).then(function (profile) { + console.log('[oneOrCreate] profile:', profile); + if (profile) { return profile; } + + return Profiles.create(req, res, tok, tok); + }); + }; + Profiles.getOrCreate = function (req, res, cred) { + var sub; + var iss; + var tok; + if (cred.accountIdx) { + sub = cred.accountIdx.split('@')[0]; + iss = cred.accountIdx.split('@')[1]; + } + tok = { sub: sub || cred.sub, iss: iss || cred.iss }; + + return Profiles.ids(req, res, cred).then(function (joins) { + if (joins.length) { + console.log('[DEBUG] CredentialsProfiles:'); + console.log(joins); + console.log('[DEBUG] will not create profile'); + return Profiles.getFromIds(req, res, joins); + } + + console.log('[DEBUG] will create profile'); + req.body.sub = req.body.sub || cred.sub; + req.body.iss = req.body.iss || cred.iss; + return Profiles.create(req, res, req.oauth3.token, req.body).then(function (profile) { + return [ profile ]; + }); + }); + }; restful.getProfile = function (req, res) { - var promise = req.Models.IssuerOauth3OrgProfiles.get(req.oauth3._IDX_ || req.oauth3.accountIdx).then(function (result) { - if (!result) { return { id: undefined }; } + // return Profiles.getOrCreate(); + console.log('[getProfile] req.oauth3.accountIdx:', req.oauth3.accountIdx); + var promise = req.Models.IssuerOauth3OrgProfiles.get(req.oauth3.accountIdx).then(function (result) { + var err; + if (!result) { + err = new Error( + "No profile exists for '" + req.oauth3.accountIdx + "'. Please create a profile or perform dual-login to link this credential to an existing one." + ); + err.code = 'E_NO_PROFILE@oauth3.org'; + return PromiseA.reject({ message: err.message, code: err.code }); + //return { id: undefined, sub: req.oauth3.accountIdx.split('@')[0], iss: req.oauth3.accountIdx.split('@')[1] }; + } result.id = undefined; //result.prv = undefined; - return req.Models.IssuerOauth3OrgContactNodes.find({ accountId: req.oauth3.accountIdx }).then(function (nodes) { + return req.Models.IssuerOauth3OrgContactNodes.find({ accountId: req.oauth3.accountIdx }).then(filterRejectable).then(function (nodes) { result.nodes = nodes; return result; }); @@ -561,15 +671,15 @@ function create(deps, app) { console.log(req.oauth3); var body = req.body; - var promise = req.Models.IssuerOauth3OrgProfiles.get(req.oauth3._IDX_ || req.oauth3.accountIdx).then(function (result) { - //var promise = req.Models.IssuerOauth3OrgProfiles.find({ sub: req.oauth3.ppid, iss: req.experienceId }) - //var result = results[0]; + var query = { accountIdx: req.oauth3.accountIdx, sub: req.oauth3.accountIdx.split('@')[0], iss: req.oauth3.accountIdx.split('@')[1] }; + // was previously accountIdx, which should have been sub@iss anyway... + var promise = Profiles.oneOrCreate(req, res, query).then(function (result) { var changed = false; console.log('[setProfile] get gotten:'); console.log(result); - if (!result) { throw new OpErr("account could not be found"); /*result = { accountId: req.oauth3.accountIdx, displayName: '', firstName: '', lastName: '', avatarUrl: '' };*/ } + if (!result) { throw new OpErr("profile could not be found"); /*result = { accountId: req.oauth3.accountIdx, displayName: '', firstName: '', lastName: '', avatarUrl: '' };*/ } // TODO schema for validation [ 'firstName', 'lastName', 'avatarUrl', 'displayName' ].forEach(function (key) { diff --git a/rest.js b/rest.js index 247fe75..875fced 100644 --- a/rest.js +++ b/rest.js @@ -49,7 +49,7 @@ module.exports.create = function (bigconf, deps, app) { app.post( '/access_token', Accounts.restful.createToken); app.use( '/exchange_token', attachSiteModels); - app.post( '/exchange_token', Accounts.restful.exchangeToken); + app.post( '/exchange_token', Accounts.restful.createToken.exchangeToken); app.use( '/acl/profile', attachSiteModels); app.get( '/acl/profile', Accounts.restful.getProfile);