-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
444 lines (408 loc) · 16.8 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
const constants = require('./core/constants.js');
const util = require('./util/util.js');
const valproxy = require('./core/valproxy.js');
const redis = require('redis');
let redisClient = null;
let db = null;
let crypto = require('crypto');
let WXBizDataCrypt = require('./vendor/WXBizDataCrypt.js');
let request = require('./net/request.js');
let userTokenPartSeparator = '|';
/**
* Ad-hoc checking whether token is valid or not.
*
* It will check against redis db whether such token exists or not.
*
* @param {string} token Token to be checked for validity
* @return {object} Promise object. Success and failure contains nothing. Use them as indicator for result only.
*/
function isTokenValid(token) {
return new Promise(function(resolve, reject) {
if (token == null) return reject();
// check against redis db
redisClient.hgetall(token, function(err, reply) {
console.log(err);
console.log(reply);
if (err || reply == null) return reject();
else return resolve();
});
});
}
/**
* Generate token for user's identitify part.
* @param {string} openId Open id of WeChat user
* @return {string} Generated user identitfy part.
*/
function generateUserIdenPart(openId) {
return openId;
}
/**
* Generate token for session identity part.
* @param {number} timestamp Timestamp, unix timestamp.
* @return {string} Generated session identity part.
*/
function generateSessionIdenPart(timestamp) {
var part = timestamp + util.generateRandomString(6);
return crypto.createHash('sha256').update(part).digest('hex');
}
/**
* Generate token.
* It consists of 3 parts
* - user -> uses openId to identify which user such token is
* - sku -> uses as it is
* - session -> sha256(concat(timestamp, nouce)) -> used to uniquely differentiate between session
* ps. nouce is random string in length 6.
*
* The final result would be
* openid|sku|session
*/
function generateToken(openId, timestamp) {
var userIdenPart = generateUserIdenPart(openId);
var sessionIdenPart = generateSessionIdenPart(timestamp);
console.log('userIdenPart: ' + userIdenPart);
console.log('sessionIdenPart: ' + sessionIdenPart);
return userIdenPart + userTokenPartSeparator + valproxy.sku + userTokenPartSeparator + sessionIdenPart;
}
/**
* Extract openId from specified token.
*
* @param {string} userToken Token string to extract openId from
* @return {string} Extracted openId string
*/
function extractOpenId(userToken) {
return userToken.substring(0, userToken.indexOf(userTokenPartSeparator));
}
/**
* Extract sku from specified token.
* @param {string} userToken Token string to extract sku from
* @return {string} Extracted sku string. Return null when token is invalid.
*/
function extractSku(userToken) {
// split token into 3 parts
var parts = userToken.split(userTokenPartSeparator);
if (parts.length == 3) {
// return the second part
return parts[1];
}
else {
return null;
}
}
/**
* Get current timestamp.
* @return {number} Return the number of milliseconds since 1970/01/01
*/
function getTimestamp() {
return Date.now();
}
/**
* Authorize WeChat user from specified credential, then returning back status result.
*
* It will check whether user has existing active session (token) in redis or not, if not then it will also check against user db on sqlite3 whether it needs to add such user in db, before trying to add a new token generated for this session.
*
* @param {String} code WeChat user's code credential
* @param {String} encryptedData WeChat user's encryptedData credential
* @param {String} iv WeChat user's iv credential
* @return {Object} Promise object. Success contains nothing, failure contains Error object with code property for reason of why it fails.
*/
function authorize(code, encryptedData, iv) {
return new Promise((resolve, reject) => {
// check required params, missing or not
if (code == null) {
// reject with error object
reject(util.createErrorObject(constants.statusCode.requiredParamsMissingError, 'Missing code parameter'));
// return immediately
return;
}
else if (encryptedData == null) {
// reject with error object
reject(util.createErrorObject(constants.statusCode.requiredParamsMissingError, 'Missing encryptedData parameter'));
// return immediately
return;
}
else if (iv == null) {
// reject with error object
reject(util.createErrorObject(constants.statusCode.requiredParamsMissingError, 'Missing iv parameter'));
// return immediately
return;
}
// 1. It make a request online using input from appId + appSecret + code to get sessionKey and openId for checking against later.
// note: value of variable is processed inside `` here.
request.getJsonAsync(`https://api.weixin.qq.com/sns/jscode2session?appid=${valproxy.appid}&secret=${valproxy.appsecret}&js_code=${code}&grant_type=authorization_code`)
.then(function(jsonRes) {
console.log(jsonRes);
// get session key
var online_sessionKey = jsonRes.session_key;
// 2. It will extract openId from those input offline using appId + sessionKey + encryptedData + iv.
var pc = new WXBizDataCrypt(valproxy.appid, online_sessionKey);
var data = null;
// try to decrypt userInfo, if error then return now
try {
data = pc.decryptData(encryptedData, iv);
} catch (err) {
console.log('userInfo decryption error');
reject(util.createErrorObject(constants.statusCode.userInfoDecryptionError, 'Decrypt userInfo error'));
return;
}
var offline_openIdOrUnionIdIfAvailable = data.openId;
if (data.unionId != null) {
offline_openIdOrUnionIdIfAvailable = data.unionId;
}
console.log(data);
// 3. Check whether two openId matches, if not response with error. Otherwise continue.
// we just check open id pair (not union id) as if someone not follow official account, then its union id won't be present
if (jsonRes.openid !== data.openId) {
console.log('OpenID or UnionID not match');
// reject with error object
reject(util.createErrorObject(constants.statusCode.openIdOrUnionIdNotMatch, 'OpenID or UnionID not match'));
return;
}
// otherwise continue
// 4. Then it checks first whether such openId is already granted with access token in redis db for its associated sku only.
// Imagine that app can have both debug and production version thus we allow 2 instances of token to be found, one for each version.
// [as first part of digested message can be used to identify user through openId, then we search through
// all keys. Only 1 session will be allowed.]
var userIdenPart = generateUserIdenPart(offline_openIdOrUnionIdIfAvailable);
redisClient.keys(`${userIdenPart}|${valproxy.sku}|*`, function(err, replies) {
if (err) {
console.log('DB error in searching for user\'s active session in redis');
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `DB Error in searching for user's active sessions: ${err.message}`));
return;
}
else if (replies != null && replies.length > 0) {
// - If so, then it doesn't expire yet, it will immediately return that token back as response.
// [detected more than 1 active session, choose first item to return as token]
console.log('found active sessions, choose first one then return as response: ', replies[0]);
resolve(util.createSuccessObject(constants.statusCode.success, 'OK', replies[0]));
return;
}
else if (replies != null && replies.length == 0) {
// - Otherwise, it checks against user table in sqlite3 db whether or not it needs to register user as a new record.
// [there's no active session, then create one]
db.all(`SELECT * FROM user WHERE openId LIKE '${offline_openIdOrUnionIdIfAvailable}'`, function(e, rows) {
if (e) {
console.log(`error select redis: ${e.message}`);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
else {
// - Not exist, register such user in sqlite3 db + generate token in redis db. Finally return token back as response.
// [if records are empty, then we need to register such user]
if (rows == null || (rows != null && rows.length == 0)) {
console.log('3');
db.run(`INSERT INTO user (openId) values ('${offline_openIdOrUnionIdIfAvailable}')`, function(e) {
if (e) {
console.log(`error insert ${e.message}`);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
else {
console.log('generate token');
// generate token
var timestamp = Date.now();
var token = generateToken(offline_openIdOrUnionIdIfAvailable, timestamp);
redisClient.hmset(token, { ctime: timestamp }, function(e, reply) {
if (e) {
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
else {
// set expire
redisClient.expire(token, valproxy.tokenTTL);
// respond back with generated token
// we're done here
console.log('done respond back with token: ', token);
resolve(util.createSuccessObject(constants.statusCode.success, 'OK', token));
return;
}
});
}
});
}
// - Exists, then generate token and insert into redis db. Finally return token as reponse.
// [if records are not empty and has exactly 1 record]
else if (rows != null && rows.length == 1) {
console.log('case 2');
// generate token
var timestamp = Date.now();
var token = generateToken(offline_openIdOrUnionIdIfAvailable, timestamp);
redisClient.hmset(token, { ctime: timestamp }, function(e, reply) {
if (e) {
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
else {
// set expire
redisClient.expire(token, valproxy.tokenTTL);
// respond back with generated token
// we're done here
resolve(util.createSuccessObject(constants.statusCode.success, 'OK', token));
return;
}
});
}
else {
// this should not happen
reject(util.createErrorObject(constants.statusCode.unknownError, 'Unknown error after finding user record from db'));
return;
}
}
});
}
else {
// should not happen
console.log('unknown error');
reject(util.createErrorObject(constants.statusCode.unknownError, 'Unknown error after try to search for user\'s active sessions.'));
return;
}
});
})
.catch(function(e) {
// response back with error
reject(util.createErrorObject(constants.statusCode.sessionKeyAndOpenIdRequestError, `Request to get session key and open id error: ${e.message}`));
return;
});
});
}
/**
* Get a new access token.
*
* It will invalidate previous acquired access token for input userid (either openid or unionid) only for attached app then generate a new one.
*
* @param {String} userid User id either is openid or unionid
* @return {Object} Promise object. Success contains a new access token string, failure contains Error object with code property for reason of why it fails.
*/
function refreshToken(userId) {
return new Promise((resolve, reject) => {
// check required params, missing or not
if (userId == null) {
// reject with error object
reject(util.createErrorObject(constants.statusCode.requiredParamsMissingError, 'Missing userId parameter'));
// return immediately
return;
}
// check in db first whether user exists
db.all(`SELECT * FROM user WHERE openId LIKE '${userId}'`, function(e, rows) {
if (e) {
console.log(`error select redis: ${e.message}`);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
else {
// not exist, then return error
if (rows == null || (rows != null && rows.length == 0)) {
reject(util.createErrorObject(constants.statusCode.userNotExistInDB, 'Cannot find such user in DB'));
return;
}
// exist, and it should only have 1 record
else if (rows != null && rows.length == 1) {
console.log('here');
// continue operation
// find userid in redisdb, if found we will remove it to invalidate that token
redisClient.keys(`${userId}|${valproxy.sku}|*`, function(err, replies) {
// error
if (err) {
console.log('error 1', err);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${err.message}`));
return;
}
console.log('replies: ', replies);
// delete existing records if any
if (replies.length > 0) {
// remove all existing records, should have only 1
redisClient.del(replies, function(err, res) {
// error
if (err) {
console.log('error 2-1', err);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${err.message}`));
return;
}
// if done, then generate a new access token
let timestamp = Date.now();
let token = generateToken(userId, timestamp);
redisClient.hmset(token, { ctime: timestamp }, function(e, reply) {
if (e) {
console.log('error 3-1', err);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
console.log('ok now 1: ' + token);
// set TTL
redisClient.expire(token, valproxy.tokenTTL);
resolve(util.createSuccessObject(constants.statusCode.success, 'OK', token));
return;
});
});
}
// generate
else {
// if done, then generate a new access token
let timestamp = Date.now();
let token = generateToken(userId, timestamp);
redisClient.hmset(token, { ctime: timestamp }, function(e, reply) {
if (e) {
console.log('error 3-2', err);
reject(util.createErrorObject(constants.statusCode.databaseRelatedError, `Error: ${e.message}`));
return;
}
console.log('ok now 2: ' + token);
// set TTL
redisClient.expire(token, valproxy.tokenTTL);
resolve(util.createSuccessObject(constants.statusCode.success, 'OK', token));
return;
});
}
});
}
// should not happen
else {
reject(util.createErrorObject(constants.statusCode.unknownError, 'Unknown error after finding user record from db'));
return;
}
}
});
});
}
/**
* Close DB connection.
*/
function close() {
if (redisClient) {
redisClient.quit();
}
}
/**
* Initialize the module.
* @param {string} appid App id of mini-program
* @param {string} appsecret App secret of mini-program
* @param {string} sku Sku for generated token to be associated with. This will help differentiated when inspect redis db later by admin. Recommend to specify it uniquely from other projects you have. But not mandatory.
* @param {object} sqlite3DBInstance Instance of SQLite3 DB
* @param {string} redispass (optional) Redis pass if you set password for your redis db
* @param {number} tokenTTL Time-to-live in seconds. When time is up, token is not valid anymore.
* @return {object} mpauthx object that has functions ready to be called.
*/
function init(appid, appsecret, sku, sqlite3DBInstance, redispass=null, tokenTTL=259200) {
valproxy.appid = appid;
valproxy.appsecret = appsecret;
valproxy.sku = sku;
valproxy.redispass = redispass;
valproxy.tokenTTL = tokenTTL;
// create a redis client
if (valproxy.redispass != null) {
redisClient = redis.createClient( { password: valproxy.redispass });
}
else {
redisClient = redis.createClient();
}
// save sqlite3 db instance
db = sqlite3DBInstance;
return {
isTokenValid: isTokenValid,
authorize: authorize,
refreshToken: refreshToken,
extractOpenId: extractOpenId,
close: close,
constants: constants
};
}
module.exports = init;