├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── decipherer.0.test.js ├── decipherer.1.test.js ├── decipherer.2.test.js ├── fetch.test.js ├── middleware.test.js ├── mockdata.js └── params.test.js ├── dist ├── decipherer.js ├── error.js ├── fetch.js ├── index.js ├── middleware.js └── params.js ├── es6 ├── decipherer.js ├── error.js ├── fetch.js ├── index.js ├── middleware.js └── params.js ├── package-lock.json └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "node": true 5 | }, 6 | "parser": "babel-eslint", 7 | "rules": { 8 | "indent": [2, 2, {"VariableDeclarator": 2, "SwitchCase": 1}], 9 | "max-len": [2, 120, 2], 10 | "eol-last": 0, 11 | "camelcase": 2, 12 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 13 | "quote-props": [2, "as-needed", {"keywords": true}], 14 | "dot-notation": [2, {"allowKeywords": true}], 15 | "quotes": [2, "single", "avoid-escape"], 16 | "new-parens": 0, 17 | "new-cap": [2, {"newIsCap": true, "capIsNew": false}], 18 | "no-wrap-func": 0, 19 | "no-use-before-define": 0, 20 | "no-cond-assign": 0, 21 | "no-underscore-dangle": 0, 22 | "no-native-reassign": 0, 23 | "no-shadow-restricted-names": 0, 24 | "no-fallthrough": 0, 25 | "eqeqeq": 0, 26 | "comma-spacing": 0, 27 | "no-multi-spaces": 0, 28 | "key-spacing": 0, 29 | "strict": 0, 30 | "no-mixed-requires": 0, 31 | "no-loop-func": 0, 32 | "no-unused-expressions": 0, 33 | "no-unused-vars": [2, {"args": "none"}], 34 | "curly": 0, 35 | "max-nested-callbacks": [2, 4], 36 | "no-multiple-empty-lines": 2 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /coverage/ 3 | /node_modules/ 4 | *.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | - "7" 7 | script: 8 | - npm run build 9 | branches: 10 | only: 11 | - master 12 | cache: 13 | directories: 14 | - $HOME/.npm 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 xixilive@gmail.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express-weapp-auth 2 | 3 | [![Build Status](https://travis-ci.org/xixilive/express-weapp-auth.svg?branch=master)](https://travis-ci.org/xixilive/express-weapp-auth) 4 | 5 | Express middleware to decrypt wechat userInfo data for weapp login scenario. 6 | 7 | ## Installation 8 | 9 | ``` 10 | # via Github 11 | npm install xixilive/express-weapp-auth --save 12 | 13 | # via npm 14 | npm install express-weapp-auth --save 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```js 20 | // basic example 21 | import {middleware} from 'express-weapp-auth' 22 | 23 | const app = require('express')() 24 | app.post( 25 | '/session/:code', 26 | 27 | middleware('appId', 'appSecret'), 28 | 29 | (req, res, next) => { 30 | const {openId, sessionKey, userInfo} = req.weappAuth 31 | //your logic here 32 | } 33 | ) 34 | 35 | // advance example 36 | app.use( 37 | '/weapp/session/', 38 | 39 | middleware('appId', 'appSecret', (req) => { 40 | return req.body 41 | }, {dataKey: 'customDataKey'}), 42 | (req, res, next) => { 43 | const {openId, sessionKey, userInfo} = req.customDataKey 44 | //your logic here 45 | } 46 | ) 47 | ``` 48 | 49 | ## Middleware 50 | 51 | ```js 52 | // all arguments 53 | middleware('appId', 'appSecret' [, paramsResolver, options]) 54 | 55 | // without optional arguments 56 | middleware('appId', 'appSecret') 57 | 58 | // without options argument 59 | middleware('appId', 'appSecret' paramsResolver) 60 | 61 | // without paramsResolver argument 62 | middleware('appId', 'appSecret' options) 63 | ``` 64 | 65 | ### Arguments 66 | 67 | - `appId`: required, weapp app ID 68 | 69 | - `appSecret`: required, weapp app secret 70 | 71 | - `paramsResolver`: optional, a `function(req){}` to resolve auth-params for request object 72 | 73 | - `options`: optional, `{dataKey: 'the key assign to req object to store decrypted data'}` 74 | 75 | ## ParamsResolver(req) 76 | 77 | It will use a built-in default resolver to resolve params for request if there has no function passed to middleware function. and the default function resolves params in a certain priority: 78 | 79 | - `req.body` with the highest priority 80 | 81 | - `req.query` with middle priority 82 | 83 | - `req.params` with the lowest priority 84 | 85 | And it expects the resolver function to return an object value with following structure: 86 | 87 | ```js 88 | { 89 | code: 'login code', 90 | rawData: 'rawData', 91 | signature: 'signature for rawData', 92 | encryptedData: 'encrypted userInfo', 93 | iv: 'cipher/decipher vector' 94 | } 95 | ``` 96 | 97 | For more details about this, please visit [微信小程序 API](https://mp.weixin.qq.com/debug/wxadoc/dev/api/) 98 | -------------------------------------------------------------------------------- /__tests__/decipherer.0.test.js: -------------------------------------------------------------------------------- 1 | const mockdata = require('./mockdata') 2 | 3 | jest.mock('../es6/fetch', () => { 4 | return jest.fn(() => Promise.resolve({ 5 | openid: mockdata.userInfo.openId, 6 | session_key: mockdata['session_key'] 7 | })) 8 | }) 9 | 10 | const decipherer = require('../es6/decipherer')('appId', 'appSecret') 11 | 12 | describe('decipher', () => { 13 | it('should success to decrypt data', () => { 14 | return decipherer({...mockdata.params, code: 'code'}).then(data => { 15 | expect(data.openId).toEqual(mockdata.userInfo.openId) 16 | expect(data.sessionKey).toEqual(mockdata['session_key']) 17 | expect(data.userInfo).toEqual(mockdata.userInfo) 18 | }) 19 | }) 20 | 21 | it('should failed to decrypt data given invalid params', () => { 22 | return decipherer({...mockdata.params, signature: 'illegal_signature', code: 'code'}).then(data => { 23 | throw(new Error('decrypted')) 24 | }).catch(err => { 25 | expect(err.message).toBe('invalid signature') 26 | }) 27 | }) 28 | 29 | }) 30 | -------------------------------------------------------------------------------- /__tests__/decipherer.1.test.js: -------------------------------------------------------------------------------- 1 | const mockdata = require('./mockdata') 2 | 3 | jest.mock('../es6/fetch', () => { 4 | return jest.fn(() => Promise.resolve({})) 5 | }) 6 | 7 | const decipherer = require('../es6/decipherer')('appId', 'appSecret') 8 | 9 | describe('decipher', () => { 10 | 11 | it('should failed to decrypt data given invalid session', () => { 12 | return decipherer({...mockdata.params, code: 'code'}).then(data => { 13 | throw(new Error('decrypted')) 14 | }).catch(err => { 15 | expect(err.message).toBe('invalid openid or session_key') 16 | }) 17 | }) 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /__tests__/decipherer.2.test.js: -------------------------------------------------------------------------------- 1 | const mockdata = require('./mockdata') 2 | 3 | jest.mock('../es6/fetch', () => { 4 | return jest.fn(() => Promise.reject({error: 'error'})) 5 | }) 6 | 7 | const decipherer = require('../es6/decipherer')('appId', 'appSecret') 8 | 9 | describe('decipher', () => { 10 | 11 | it('should failed to decrypt data when fetch sessionKey request failed', () => { 12 | return decipherer({...mockdata.params, code: 'code'}).then(data => { 13 | throw(new Error('decrypted')) 14 | }).catch(err => { 15 | expect(err.message).toBe('jscode2session failed') 16 | }) 17 | }) 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /__tests__/fetch.test.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock') 2 | const fetchUrl = require('../es6/fetch') 3 | 4 | describe('fetch', () => { 5 | beforeEach(() => { 6 | global.fetch = undefined 7 | expect(global.fetch).toBeUndefined() 8 | }) 9 | 10 | it('should use global.fetch if it was defined', () => { 11 | global.fetch = jest.fn(() => Promise.resolve({json: () => ({ok: 1})})) 12 | return fetchUrl('https://example.com/api').then(data => { 13 | expect(global.fetch).toHaveBeenCalledWith('https://example.com/api', {method: 'GET'}) 14 | expect(data).toEqual({ok: 1}) 15 | }) 16 | }) 17 | 18 | it('should be success with native https module', () => { 19 | let nockScope = nock('https://example.com').get('/api').reply(200, {ok: 1}) 20 | return fetchUrl('https://example.com/api').then((data) => { 21 | nockScope.done() 22 | expect(data).toEqual({ok: 1}) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /__tests__/middleware.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../es6/fetch', () => { 2 | return jest.fn(() => Promise.resolve({ 3 | openid: mockdata.userInfo.openId, 4 | session_key: mockdata['session_key'] 5 | })) 6 | }) 7 | 8 | const Middleware = require('../es6').middleware 9 | const mockdata = require('./mockdata') 10 | const mockNext = jest.fn() 11 | 12 | describe('middleware', () => { 13 | it('with default config', () => { 14 | const middleware = Middleware('appId', 'appSecret') 15 | const req = {body: {...mockdata.params, code: 'code'}} 16 | return middleware(req, {}, mockNext).then(() => { 17 | expect(req.weappAuth).toBeDefined() 18 | expect(req.weappAuth.openId).toEqual(mockdata.userInfo.openId) 19 | expect(req.weappAuth.sessionKey).toEqual(mockdata.session_key) 20 | expect(req.weappAuth.userInfo).toEqual(mockdata.userInfo) 21 | expect(mockNext).toHaveBeenCalledTimes(1) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /__tests__/mockdata.js: -------------------------------------------------------------------------------- 1 | const userInfo = { 2 | openId: 'oGZUI0egBJY1zhBYw2KhdUfwVJJE', 3 | nickName: 'Band', 4 | gender: 1, 5 | language: 'zh_CN', 6 | city: 'Guangzhou', 7 | province: 'Guangdong', 8 | country: 'CN', 9 | avatarUrl: 'http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0', 10 | unionId: 'ocMvos6NjeKLIBqg5Mr9QjxrP1FA', 11 | watermark: { 12 | timestamp: 1477314187, 13 | appid: 'wx4f4bc4dec97d474b' 14 | } 15 | } 16 | 17 | const encryptedData = [ 18 | 'CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZM', 19 | 'QmRzooG2xrDcvSnxIMXFufNstNGTyaGS', 20 | '9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+', 21 | '3hVbJSRgv+4lGOETKUQz6OYStslQ142d', 22 | 'NCuabNPGBzlooOmB231qMM85d2/fV6Ch', 23 | 'evvXvQP8Hkue1poOFtnEtpyxVLW1zAo6', 24 | '/1Xx1COxFvrc2d7UL/lmHInNlxuacJXw', 25 | 'u0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn', 26 | '/Hz7saL8xz+W//FRAUid1OksQaQx4CMs', 27 | '8LOddcQhULW4ucetDf96JcR3g0gfRK4P', 28 | 'C7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB', 29 | '6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns', 30 | '/8wR2SiRS7MNACwTyrGvt9ts8p12PKFd', 31 | 'lqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYV', 32 | 'oKlaRv85IfVunYzO0IKXsyl7JCUjCpoG', 33 | '20f0a04COwfneQAGGwd5oa+T8yO5hzuy', 34 | 'Db/XcxxmK01EpqOyuxINew==' 35 | ].join('') 36 | 37 | const params = { 38 | iv: 'r7BXXKkLb8qrSNn05n0qiA==', 39 | rawData: JSON.stringify(userInfo), 40 | signature: 'f522fa59640195c57eaa33eceb8cfa5d2b5d4d68', 41 | encryptedData 42 | } 43 | 44 | const session_key = 'tiihtNczf5v6AKRyjwEUhQ==' 45 | 46 | export default { 47 | userInfo, params, session_key 48 | } 49 | -------------------------------------------------------------------------------- /__tests__/params.test.js: -------------------------------------------------------------------------------- 1 | const authParams = require('../es6/params') 2 | 3 | describe('resolve auth params', () => { 4 | 5 | const expectParams = (expectation) => (params) => { 6 | expect(params.code).toBe(expectation.code) 7 | expect(params.rawData).toBe(expectation.rawData) 8 | expect(params.signature).toBe(expectation.signature) 9 | expect(params.encryptedData).toBe(expectation.encryptedData) 10 | expect(params.iv).toBe(expectation.iv) 11 | } 12 | 13 | const expectInvalidParams = (invalidParams) => () => { 14 | return authParams({}, () => invalidParams).then(params => { 15 | throw(new Error('resolved')) 16 | }).catch(err => { 17 | expect(err).toBeDefined() 18 | expect(err.message).toBe('invalid auth params') 19 | expect(err.statusCode).toBe(400) 20 | }) 21 | } 22 | 23 | it('should be resolved with default resolver', () => { 24 | const req = { 25 | body: {code: 'code', rawData: 'rawData', signature: 'signature', encryptedData: 'encryptedData', iv: 'iv'}, 26 | } 27 | return authParams(req).then(expectParams(req.body)) 28 | }) 29 | 30 | it('should be resolved with default resolver in specified priority: body, query, and path', () => { 31 | const req = { 32 | params: {code: 'code0'}, 33 | query: {code: 'code1'}, 34 | body: {code: 'code2', rawData: 'rawData', signature: 'signature', encryptedData: 'encryptedData', iv: 'iv'}, 35 | } 36 | return authParams(req).then(expectParams(req.body)) 37 | }) 38 | 39 | it('should be resolved with customized resolver', () => { 40 | const params = { 41 | code: 'code', 42 | rawData: 'rawData', 43 | signature: 'signature', 44 | encryptedData: 'encryptedData', 45 | iv: 'iv' 46 | } 47 | const paramsResolver = () => params 48 | return authParams({}, paramsResolver).then(expectParams(params)) 49 | }) 50 | 51 | describe('nulla params', () => { 52 | it('should not be resolved', expectInvalidParams()) 53 | it('should not be resolved', expectInvalidParams(null)) 54 | }) 55 | 56 | describe('empty params', () => { 57 | it('should not be resolved', expectInvalidParams({})) 58 | it('should not be resolved', expectInvalidParams(() => {})) 59 | }) 60 | 61 | describe('type-error params', () => { 62 | it('should not be resolved', expectInvalidParams([])) 63 | it('should not be resolved', expectInvalidParams('')) 64 | }) 65 | 66 | }) 67 | -------------------------------------------------------------------------------- /dist/decipherer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | var _crypto = require('crypto'); 10 | 11 | var _error = require('./error'); 12 | 13 | var _error2 = _interopRequireDefault(_error); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var fetchUrl = require('./fetch'); 18 | 19 | var signatureRawData = function signatureRawData(rawData, sessionKey) { 20 | var data = '' + rawData + sessionKey; 21 | return (0, _crypto.createHash)('sha1').update(data).digest('hex'); 22 | }; 23 | 24 | var decryptUserData = function decryptUserData(encryptedData, iv, sessionKey) { 25 | var buffers = { 26 | data: new Buffer(encryptedData, 'base64'), 27 | key: new Buffer(sessionKey, 'base64'), 28 | iv: new Buffer(iv, 'base64') 29 | }; 30 | 31 | return new Promise(function (resolve, reject) { 32 | try { 33 | var decipher = (0, _crypto.createDecipheriv)('aes-128-cbc', buffers.key, buffers.iv); 34 | decipher.setAutoPadding(true); 35 | 36 | var decoded = decipher.update(buffers.data, 'binary', 'utf8'); 37 | decoded += decipher.final('utf8'); 38 | 39 | resolve(JSON.parse(decoded)); 40 | } catch (err) { 41 | reject(err); 42 | } 43 | }).catch(function (err) { 44 | throw (0, _error2.default)(500, 'decrypt user data failed'); 45 | }); 46 | }; 47 | 48 | var getSessionKey = function getSessionKey(appId, appSecret, code) { 49 | var url = 'https://api.weixin.qq.com/sns/jscode2session'; 50 | url += '?appid=' + appId + '&secret=' + appSecret + '&js_code=' + code; 51 | url += '&grant_type=authorization_code'; 52 | 53 | return fetchUrl(url).then(function (response) { 54 | return { openId: response.openid, sessionKey: response.session_key }; 55 | }).catch(function (err) { 56 | throw (0, _error2.default)(400, 'jscode2session failed'); 57 | }); 58 | }; 59 | 60 | var decipherer = function decipherer(appId, appSecret) { 61 | return function (params) { 62 | var code = params.code, 63 | rawData = params.rawData, 64 | signature = params.signature, 65 | encryptedData = params.encryptedData, 66 | iv = params.iv; 67 | 68 | return getSessionKey(appId, appSecret, code).then(function (_ref) { 69 | var openId = _ref.openId, 70 | sessionKey = _ref.sessionKey; 71 | 72 | if (!openId || !sessionKey) { 73 | return Promise.reject((0, _error2.default)(400, 'invalid openid or session_key')); 74 | } 75 | 76 | if (signature !== signatureRawData(rawData, sessionKey)) { 77 | return Promise.reject((0, _error2.default)(400, 'invalid signature')); 78 | } 79 | 80 | return Promise.all([openId, sessionKey, decryptUserData(encryptedData, iv, sessionKey)]); 81 | }).then(function (_ref2) { 82 | var _ref3 = _slicedToArray(_ref2, 3), 83 | openId = _ref3[0], 84 | sessionKey = _ref3[1], 85 | userInfo = _ref3[2]; 86 | 87 | return { openId: openId, sessionKey: sessionKey, userInfo: userInfo }; 88 | }); 89 | }; 90 | }; 91 | 92 | exports.default = decipherer; 93 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (httpStatus, message) { 8 | var error = new Error(message); 9 | error.statusCode = httpStatus; 10 | return error; 11 | }; 12 | 13 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /dist/fetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = fetchUrl; 7 | 8 | var _error = require('./error'); 9 | 10 | var _error2 = _interopRequireDefault(_error); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var https = require('https'); 15 | 16 | var get = function get(url) { 17 | return new Promise(function (resolve, reject) { 18 | https.get(url, function (res) { 19 | var statusCode = res.statusCode; 20 | 21 | if (statusCode < 200 || statusCode >= 300) { 22 | res.resume(); 23 | return reject((0, _error2.default)(statusCode, 'Request failed')); 24 | } 25 | 26 | res.setEncoding('utf8'); 27 | var rawData = ''; 28 | res.on('data', function (chunk) { 29 | return rawData += chunk; 30 | }); 31 | res.on('end', function () { 32 | try { 33 | resolve(JSON.parse(rawData)); 34 | } catch (err) { 35 | reject((0, _error2.default)(500, 'Request failed')); 36 | } 37 | }); 38 | }); 39 | }); 40 | }; 41 | 42 | function fetchUrl(url) { 43 | if ('function' === typeof fetch) { 44 | return fetch(url, { method: 'GET' }).then(function (res) { 45 | return res.json(); 46 | }); 47 | } 48 | return get(url); 49 | } 50 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _decipherer = require('./decipherer'); 8 | 9 | Object.defineProperty(exports, 'decipherer', { 10 | enumerable: true, 11 | get: function get() { 12 | return _interopRequireDefault(_decipherer).default; 13 | } 14 | }); 15 | 16 | var _params = require('./params'); 17 | 18 | Object.defineProperty(exports, 'resolveParams', { 19 | enumerable: true, 20 | get: function get() { 21 | return _interopRequireDefault(_params).default; 22 | } 23 | }); 24 | 25 | var _middleware = require('./middleware'); 26 | 27 | Object.defineProperty(exports, 'middleware', { 28 | enumerable: true, 29 | get: function get() { 30 | return _interopRequireDefault(_middleware).default; 31 | } 32 | }); 33 | 34 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /dist/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _decipherer = require('./decipherer'); 8 | 9 | var _decipherer2 = _interopRequireDefault(_decipherer); 10 | 11 | var _params = require('./params'); 12 | 13 | var _params2 = _interopRequireDefault(_params); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var middleware = function middleware(appId, appSecret, paramsResolver) { 18 | var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; 19 | 20 | if ('[object Object]' !== Object.prototype.toString.call(paramsResolver)) { 21 | options = paramsResolver; 22 | paramsResolver = null; 23 | } 24 | 25 | var _ref = options || {}, 26 | _ref$dataKey = _ref.dataKey, 27 | dataKey = _ref$dataKey === undefined ? 'weappAuth' : _ref$dataKey; 28 | 29 | return function (req, res, next) { 30 | return (0, _params2.default)(req, paramsResolver).then((0, _decipherer2.default)(appId, appSecret)).then(function (data) { 31 | req[dataKey] = data; 32 | next(); 33 | }).catch(next); 34 | }; 35 | }; 36 | 37 | exports.default = middleware; 38 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/params.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | var _error = require('./error'); 10 | 11 | var _error2 = _interopRequireDefault(_error); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var namedParams = ['rawData', 'signature', 'encryptedData', 'iv', 'code']; 16 | 17 | var defaultParamsResolver = function defaultParamsResolver(req) { 18 | var _req$params = req.params, 19 | params = _req$params === undefined ? {} : _req$params, 20 | _req$query = req.query, 21 | query = _req$query === undefined ? {} : _req$query, 22 | _req$body = req.body, 23 | body = _req$body === undefined ? {} : _req$body; 24 | 25 | return namedParams.reduce(function (mem, name) { 26 | var val = body[name] || query[name] || params[name]; 27 | if ('string' === typeof val && val.trim() !== '') { 28 | mem[name] = val; 29 | } 30 | return mem; 31 | }, {}); 32 | }; 33 | 34 | var validateParams = function validateParams(params) { 35 | var type = typeof params === 'undefined' ? 'undefined' : _typeof(params); 36 | if (params === null || type !== 'object' && type !== 'function') { 37 | return false; 38 | } 39 | 40 | var rawData = params.rawData, 41 | signature = params.signature, 42 | encryptedData = params.encryptedData, 43 | iv = params.iv, 44 | code = params.code; 45 | 46 | return Boolean(code && signature && encryptedData && rawData && iv); 47 | }; 48 | 49 | var authParamsResolver = function authParamsResolver(req, resolver) { 50 | if ('function' !== typeof resolver) { 51 | resolver = defaultParamsResolver; 52 | } 53 | return new Promise(function (resolve, reject) { 54 | var params = resolver(req); 55 | validateParams(params) ? resolve(params) : reject((0, _error2.default)(400, 'invalid auth params')); 56 | }); 57 | }; 58 | 59 | exports.default = authParamsResolver; 60 | module.exports = exports['default']; -------------------------------------------------------------------------------- /es6/decipherer.js: -------------------------------------------------------------------------------- 1 | import {createDecipheriv, createHash} from 'crypto' 2 | import httpError from './error' 3 | 4 | const fetchUrl = require('./fetch') 5 | 6 | const signatureRawData = (rawData, sessionKey) => { 7 | const data = `${rawData}${sessionKey}` 8 | return createHash('sha1').update(data).digest('hex') 9 | } 10 | 11 | const decryptUserData = (encryptedData, iv, sessionKey) => { 12 | const buffers = { 13 | data: new Buffer(encryptedData, 'base64'), 14 | key: new Buffer(sessionKey, 'base64'), 15 | iv: new Buffer(iv, 'base64') 16 | } 17 | 18 | return new Promise((resolve, reject) => { 19 | try { 20 | let decipher = createDecipheriv('aes-128-cbc', buffers.key, buffers.iv) 21 | decipher.setAutoPadding(true) 22 | 23 | let decoded = decipher.update(buffers.data, 'binary', 'utf8') 24 | decoded += decipher.final('utf8') 25 | 26 | resolve(JSON.parse(decoded)) 27 | }catch(err){ 28 | reject(err) 29 | } 30 | }).catch(err => { 31 | throw(httpError(500, 'decrypt user data failed')) 32 | }) 33 | } 34 | 35 | const getSessionKey = (appId, appSecret, code) => { 36 | let url = 'https://api.weixin.qq.com/sns/jscode2session' 37 | url += `?appid=${appId}&secret=${appSecret}&js_code=${code}` 38 | url += '&grant_type=authorization_code' 39 | 40 | return fetchUrl(url).then(response => { 41 | return {openId: response.openid, sessionKey: response.session_key} 42 | }).catch(err => { 43 | throw(httpError(400, 'jscode2session failed')) 44 | }) 45 | } 46 | 47 | const decipherer = (appId, appSecret) => (params) => { 48 | const {code, rawData, signature, encryptedData, iv} = params 49 | return getSessionKey(appId, appSecret, code) 50 | .then(({openId, sessionKey}) => { 51 | if(!openId || !sessionKey){ 52 | return Promise.reject(httpError(400, 'invalid openid or session_key')) 53 | } 54 | 55 | if(signature !== signatureRawData(rawData, sessionKey)){ 56 | return Promise.reject(httpError(400, 'invalid signature')) 57 | } 58 | 59 | return Promise.all([ 60 | openId, sessionKey, decryptUserData(encryptedData, iv, sessionKey) 61 | ]) 62 | }) 63 | .then(([openId, sessionKey, userInfo]) => { 64 | return {openId, sessionKey, userInfo} 65 | }) 66 | } 67 | 68 | export default decipherer 69 | -------------------------------------------------------------------------------- /es6/error.js: -------------------------------------------------------------------------------- 1 | export default function(httpStatus, message){ 2 | const error = new Error(message) 3 | error.statusCode = httpStatus 4 | return error 5 | } 6 | -------------------------------------------------------------------------------- /es6/fetch.js: -------------------------------------------------------------------------------- 1 | import httpError from './error' 2 | 3 | const https = require('https') 4 | 5 | const get = (url) => { 6 | return new Promise((resolve, reject) => { 7 | https.get(url, (res) => { 8 | const {statusCode} = res 9 | if(statusCode < 200 || statusCode >= 300){ 10 | res.resume() 11 | return reject(httpError(statusCode, 'Request failed')) 12 | } 13 | 14 | res.setEncoding('utf8') 15 | let rawData = '' 16 | res.on('data', chunk => rawData += chunk) 17 | res.on('end', () => { 18 | try { 19 | resolve(JSON.parse(rawData)) 20 | } catch (err) { 21 | reject(httpError(500, 'Request failed')) 22 | } 23 | }) 24 | }) 25 | }) 26 | } 27 | 28 | export default function fetchUrl(url){ 29 | if('function' === typeof fetch){ 30 | return fetch(url, {method: 'GET'}).then(res => res.json()) 31 | } 32 | return get(url) 33 | } 34 | -------------------------------------------------------------------------------- /es6/index.js: -------------------------------------------------------------------------------- 1 | export {default as decipherer} from './decipherer' 2 | export {default as resolveParams} from './params' 3 | export {default as middleware} from './middleware' 4 | -------------------------------------------------------------------------------- /es6/middleware.js: -------------------------------------------------------------------------------- 1 | import decipherer from './decipherer' 2 | import resolveParams from './params' 3 | 4 | const middleware = ( 5 | appId, appSecret, paramsResolver, options = {} 6 | ) => { 7 | if('[object Object]' !== Object.prototype.toString.call(paramsResolver)){ 8 | options = paramsResolver 9 | paramsResolver = null 10 | } 11 | 12 | const {dataKey = 'weappAuth'} = options || {} 13 | 14 | return (req, res, next) => { 15 | return resolveParams(req, paramsResolver) 16 | .then(decipherer(appId, appSecret)) 17 | .then(data => { 18 | req[dataKey] = data 19 | next() 20 | }) 21 | .catch(next) 22 | } 23 | } 24 | 25 | export default middleware -------------------------------------------------------------------------------- /es6/params.js: -------------------------------------------------------------------------------- 1 | import httpError from './error' 2 | 3 | const namedParams = [ 4 | 'rawData', 'signature', 'encryptedData', 'iv', 'code' 5 | ] 6 | 7 | const defaultParamsResolver = (req) => { 8 | const {params = {}, query = {}, body = {}} = req 9 | return namedParams.reduce((mem, name) => { 10 | const val = body[name] || query[name] || params[name] 11 | if('string' === typeof val && val.trim() !== ''){ 12 | mem[name] = val 13 | } 14 | return mem 15 | }, {}) 16 | } 17 | 18 | const validateParams = (params) => { 19 | const type = typeof params 20 | if(params === null || (type !== 'object' && type !== 'function')){ 21 | return false 22 | } 23 | 24 | const {rawData, signature, encryptedData, iv, code} = params 25 | return Boolean(code && signature && encryptedData && rawData && iv) 26 | } 27 | 28 | const authParamsResolver = (req, resolver) => { 29 | if('function' !== typeof resolver){ 30 | resolver = defaultParamsResolver 31 | } 32 | return new Promise((resolve, reject) => { 33 | const params = resolver(req) 34 | validateParams(params) ? 35 | resolve(params) : 36 | reject(httpError(400, 'invalid auth params')) 37 | }) 38 | } 39 | 40 | export default authParamsResolver 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-weapp-auth", 3 | "version": "0.2.0", 4 | "description": "Express middleware to decrypt wechat userInfo data for weapp(微信小程序) login scenario.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "pretest": "eslint es6", 8 | "test": "jest --coverage", 9 | "build": "npm test && rimraf dist/* && babel --copy-files ./es6 -d ./dist", 10 | "prepublish": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/xixilive/express-weapp-auth.git" 15 | }, 16 | "keywords": [ 17 | "weixin", 18 | "wechat", 19 | "weapp", 20 | "authentication", 21 | "express", 22 | "middleware", 23 | "微信小程序" 24 | ], 25 | "author": "xixilive@gmail.com", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/xixilive/express-weapp-auth/issues" 29 | }, 30 | "homepage": "https://github.com/xixilive/express-weapp-auth#readme", 31 | "devDependencies": { 32 | "babel-cli": "^6.22.2", 33 | "babel-core": "^6.22.1", 34 | "babel-eslint": "^7.1.1", 35 | "babel-jest": "^18.0.0", 36 | "babel-plugin-add-module-exports": "^0.2.1", 37 | "babel-preset-es2015": "^6.22.0", 38 | "babel-preset-stage-0": "^6.22.0", 39 | "eslint": "^3.14.0", 40 | "jest-cli": "^18.1.0", 41 | "nock": "^9.0.2", 42 | "rimraf": "^2.5.4" 43 | }, 44 | "jest": { 45 | "automock": false, 46 | "bail": false, 47 | "transform": { 48 | ".js": "/node_modules/babel-jest" 49 | }, 50 | "testPathDirs": [ 51 | "/__tests__/" 52 | ], 53 | "unmockedModulePathPatterns": [ 54 | "/node_modules/" 55 | ], 56 | "testPathIgnorePatterns": [ 57 | "/node_modules/" 58 | ], 59 | "testRegex": [ 60 | ".test.js$" 61 | ] 62 | } 63 | } 64 | --------------------------------------------------------------------------------