Compare commits
	
		
			10 Commits
		
	
	
		
			master
			...
			v1.2-profi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8471f6b6ca | |||
| f18dab15b7 | |||
| af0ef74f23 | |||
| efae8caf3b | |||
| c152878201 | |||
| abaa59dab0 | |||
| 81b213ec4b | |||
| 8397b8a38c | |||
| 3fedb3d8ad | |||
| 9627e2054e | 
| @ -5,7 +5,7 @@ issuer@oauth3.org (js) | ||||
| | [issuer.html](https://git.oauth3.org/OAuth3/issuer.html) | ||||
| | *issuer.rest.walnut.js* | ||||
| | [issuer.srv](https://git.oauth3.org/OAuth3/issuer.srv) | ||||
| | Sponsored by [ppl](https://ppl.family) | ||||
| | Sponsored by [Daplie](https://daplie.com) | ||||
| 
 | ||||
| Implementation of server-side RESTful OAuth3 issuer APIs. | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										368
									
								
								accounts.js
									
									
									
									
									
								
							
							
						
						
									
										368
									
								
								accounts.js
									
									
									
									
									
								
							| @ -61,19 +61,26 @@ function validateOtp(codeStore, codeId, token) { | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function getOrCreate(store, username) { | ||||
|   return store.IssuerOauth3OrgAccounts.get(username).then(function (account) { | ||||
|     if (account) { | ||||
|       return account; | ||||
| function getOrCreate(store, iss, username) { | ||||
|   // account => profile
 | ||||
|   return store.IssuerOauth3OrgProfiles.find({ username: username }).then(filterRejectable).then(function (profile) { | ||||
|     profile = profile && profile[0]; | ||||
|     if (profile) { | ||||
|       return profile; | ||||
|     } | ||||
| 
 | ||||
|     account = { | ||||
|       username:  username, | ||||
|       accountId: makeB64UrlSafe(crypto.randomBytes(32).toString('base64')), | ||||
|     // TODO profile should be ecdsa256 pub/privkeypair
 | ||||
|     var sub = makeB64UrlSafe(crypto.randomBytes(32).toString('base64')); | ||||
|     profile = { | ||||
|       id: sub + (iss && ('@' + iss) || '') | ||||
|     , username:  username | ||||
|     , sub: sub | ||||
|     , iss: iss | ||||
|     , typ: username ? 'username' : 'profile' | ||||
|     }; | ||||
|     return store.IssuerOauth3OrgAccounts.create(username, account).then(function () { | ||||
|     return store.IssuerOauth3OrgProfiles.create(profile.id, profile).then(function () { | ||||
|       // TODO: put sort sort of email notification to the server managers?
 | ||||
|       return account; | ||||
|       return profile; | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @ -167,18 +174,28 @@ function createOtp(store, params) { | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| function rejectDeleted(el) { | ||||
|   if (el && (!el.revokedAt && !el.deletedAt)) { | ||||
|     return el; | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
| function filterRejectable(arr) { | ||||
|   return arr.filter(rejectDeleted); | ||||
| } | ||||
| 
 | ||||
| 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, | ||||
|           from:    'login@daplie.com', | ||||
|           replyTo: 'hello@daplie.com', | ||||
|           from:    'login@mg.hellabit.com', | ||||
|           replyTo: 'hello@mg.hellabit.com', | ||||
|           subject: "Use " + code.code + " as your Login Code", | ||||
|           text: "Your login code is:\n\n" | ||||
|                 + code.code | ||||
| @ -206,6 +223,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"); | ||||
|       } | ||||
| @ -219,9 +237,20 @@ function create(app) { | ||||
|       if (req.body.grant_type === 'refresh_token') { | ||||
|         return restful.createToken.refreshToken(req); | ||||
|       } | ||||
|       if (req.body.grant_type === 'exchange_token') { | ||||
|         return restful.createToken.exchangeToken(req); | ||||
|       } | ||||
| 
 | ||||
|       throw new OpErr("unknown or un-implemented grant_type '"+req.body.grant_type+"'"); | ||||
|     }).then(function (token_info) { | ||||
|       return restful.createToken._helper(req, res, token_info); | ||||
|     }); | ||||
| 
 | ||||
|     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; | ||||
|       if (!token_info.aud) { | ||||
|         throw new OpErr("missing required token field 'aud'"); | ||||
| @ -234,17 +263,18 @@ function create(app) { | ||||
|         // We don't have normal grants for the issuer, so we don't need to look the
 | ||||
|         // azpSub or the grants up in the database.
 | ||||
|         token_info.azpSub = token_info.sub; | ||||
|         token_info.scope = ''; | ||||
|         token_info.scope = '*'; | ||||
|         return token_info; | ||||
|       } | ||||
| 
 | ||||
|       var search = {}; | ||||
|       ['sub', 'azp', 'azpSub'].forEach(function (key) { | ||||
|       [ 'sub', 'azp', 'azpSub' ].forEach(function (key) { | ||||
|         if (token_info[key]) { | ||||
|           search[key] = token_info[key]; | ||||
|         } | ||||
|       }); | ||||
|       return store.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)+"'"); | ||||
|         } | ||||
| @ -258,7 +288,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
 | ||||
| @ -300,9 +330,49 @@ function create(app) { | ||||
|         return result; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     app.handlePromise(req, res, promise, '[issuer@oauth3.org] create tokens'); | ||||
|   }; | ||||
| 
 | ||||
|   // 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) { | ||||
| @ -319,22 +389,24 @@ 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); | ||||
|         return getOrCreate(store, req.experienceId, params.username); | ||||
|       }).then(function (account) { | ||||
|         var contactClaimId = crypto.createHash('sha256').update(account.accountId+':'+params.username_type+':'+params.username).digest('base64'); | ||||
|         var contactClaimId = crypto.createHash('sha256').update(account.sub+':'+params.username_type+':'+params.username).digest('base64'); | ||||
|         //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) { contactClaim = { id: contactClaimId, accountId: (req.oauth3._IDX_ || req.oauth3.accountIdx) }; } | ||||
|           if (!contactClaim.verifiedAt) { contactClaim.verifiedAt = now; } | ||||
|           contactClaim.lastVerifiedAt = now; | ||||
| 
 | ||||
|           console.log('contactClaim'); | ||||
|           console.log('[DEBUG] contactClaim'); | ||||
|           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, | ||||
|             }; | ||||
| @ -371,30 +443,242 @@ function create(app) { | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
| 
 | ||||
|   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.IssuerOauth3OrgAccounts.get(req.oauth3.accountIdx).then(function (result) { | ||||
|       if (!result) { result = { 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 result; | ||||
|       return req.Models.IssuerOauth3OrgContactNodes.find({ accountId: req.oauth3.accountIdx }).then(filterRejectable).then(function (nodes) { | ||||
|         result.nodes = nodes; | ||||
|         return result; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     app.handlePromise(req, res, promise, '[issuer@oauth3.org] get profile'); | ||||
|   }; | ||||
|   restful.setProfile = function (req, res) { | ||||
|     console.log('req.oauth3'); | ||||
|     console.log('[setProfile] 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 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('get gotten'); | ||||
|       console.log(results); | ||||
|       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) { | ||||
| @ -406,18 +690,33 @@ function create(app) { | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       if (body.email && (result.email !== body.email)) { | ||||
|         if (result.unverifiedEmail !== body.email) { | ||||
|           changed = true; | ||||
|           result.unverifiedEmail = body.email; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (changed) { | ||||
|         return req.Models.IssuerOauth3OrgAccounts.upsert(result).then(function () { console.log('update updated'); return result; }); | ||||
|         return req.Models.IssuerOauth3OrgProfiles.upsert(result).then(function () { console.log('[setProfile] update updated'); return result; }); | ||||
|       } | ||||
| 
 | ||||
|       return result; | ||||
|     }).then(function (result) { | ||||
|       result.id = undefined; | ||||
|       //result.prv = undefined;
 | ||||
|       return result; | ||||
|     }); | ||||
| 
 | ||||
|     app.handlePromise(req, res, promise, '[issuer@oauth3.org] set profile'); | ||||
|   }; | ||||
|   restful.listContactNodes = function (req, res) { | ||||
|     /* | ||||
|     var contactClaimId = crypto.createHash('sha256').update((req.oauth3._IDX_ || req.oauth3.accountIdx)+':'+code.node.type+':'+code.node.node).digest('base64'); | ||||
|     return req.Models.IssuerOauth3OrgContactNodes.get(contactClaimId).then(function (contactClaim) { | ||||
|       return; | ||||
|     }); | ||||
|     */ | ||||
|   }; | ||||
|   restful.claimContact = function (req, res) { | ||||
|     var type = req.body.type; | ||||
| @ -458,7 +757,8 @@ function create(app) { | ||||
|         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'); | ||||
|       // TODO this token may represent a 3rd-party credential or 1st-party profile. What should the ID be?
 | ||||
|       var contactClaimId = crypto.createHash('sha256').update((req.oauth3._IDX_ || 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 }; } | ||||
|  | ||||
							
								
								
									
										26
									
								
								models.js
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								models.js
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var apiname = 'issuer_oauth3_org'; | ||||
| var baseFields = [ 'createdAt', 'updatedAt', 'deletedAt' ]; | ||||
| var baseFields = [ 'createdAt', 'updatedAt', 'deletedAt', 'revokedAt', 'insertedAt' ]; | ||||
| 
 | ||||
| module.exports = [ | ||||
|   { | ||||
| @ -15,14 +15,28 @@ module.exports = [ | ||||
|     indices: baseFields.concat([ 'code', 'expires' ]), | ||||
|   }, | ||||
|   { | ||||
|     tablename: apiname + '_accounts', | ||||
|     idname: 'username', | ||||
|     indices: baseFields.concat([ 'accountId' ]), | ||||
|     tablename: apiname + '_credentials', | ||||
|     idname: 'id', | ||||
|     // credentialId = ppid@iss
 | ||||
|     indices: baseFields.concat([ 'username', 'sub', 'iss', 'typ', 'salt', 'shadow' ]), // comment, recoveryCredential
 | ||||
|   }, | ||||
|   { | ||||
|     tablename: apiname + '_credentials_profiles', | ||||
|     idname: 'id', | ||||
|     // credentialId = ppid@iss
 | ||||
|     indices: baseFields.concat([ 'credentialId', 'profileId' ]), | ||||
|   }, | ||||
|   { | ||||
|     tablename: apiname + '_profiles', | ||||
|     idname: 'id', | ||||
|     // make sub an ecdsa256 key
 | ||||
|     indices: baseFields.concat([ 'username', 'sub', 'iss', 'typ', 'privateKey' ]), // comment, recoveryNodes
 | ||||
|   }, | ||||
|   { | ||||
|     tablename: apiname + '_contact_nodes', | ||||
|     idname: 'id', | ||||
|     indices: baseFields.concat([ 'accountId', 'verifiedAt', 'lastVerifiedAt' ]), | ||||
|     // contact nodes could apply to either credential or profile?
 | ||||
|     indices: baseFields.concat([ 'accountId', 'priority', 'verifiedAt', 'lastVerifiedAt' ]), | ||||
|   }, | ||||
|   { | ||||
|     tablename: apiname + '_jwks', | ||||
| @ -33,5 +47,5 @@ module.exports = [ | ||||
|     tablename: apiname + '_grants', | ||||
|     idname: 'id', | ||||
|     indices: baseFields.concat([ 'sub', 'azp', 'azpSub', 'scope' ]), | ||||
|   }, | ||||
|   } | ||||
| ]; | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
							
								
								
									
										10
									
								
								rest.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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.
 | ||||
| @ -48,11 +48,15 @@ module.exports.create = function (bigconf, deps, app) { | ||||
|   app.post(  '/access_token/:sub/:aud/:azp',    Accounts.restful.createToken); | ||||
|   app.post(  '/access_token',                   Accounts.restful.createToken); | ||||
| 
 | ||||
|   app.use(   '/acl/profile',                    attachSiteModels); | ||||
|   app.use(   '/exchange_token',                 attachSiteModels); | ||||
|   app.post(  '/exchange_token',                 Accounts.restful.createToken.exchangeToken); | ||||
| 
 | ||||
|   // TODO secure ACL endpoints with proper grants
 | ||||
|   app.use(   '/acl/profile',                    attachSiteModels, /*app.grantsRequired(['profile@oauth3.org'])*/); | ||||
|   app.get(   '/acl/profile',                    Accounts.restful.getProfile); | ||||
|   app.post(  '/acl/profile',                    Accounts.restful.setProfile); | ||||
| 
 | ||||
|   app.use(   '/acl/contact_nodes',              attachSiteModels); | ||||
|   app.use(   '/acl/contact_nodes',              attachSiteModels, /*app.grantsRequired(['profile@oauth3.org'])*/); | ||||
|   app.post(  '/acl/contact_nodes',              Accounts.restful.claimContact); | ||||
|   app.post(  '/acl/contact_nodes/:id',          Accounts.restful.verifyContact); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user