├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── MIT-License ├── Makefile ├── README.md ├── lib ├── oauth.js ├── util.js └── wx_biz_data_crypt.js ├── package.json └── test ├── config.js ├── oauth.test.js └── wx_biz_data_crypt.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.debug.js 2 | *.min.js 3 | node_modules/* 4 | assets/scripts/lib/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [2, 2], 4 | "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 5 | "linebreak-style": [2, "unix"], 6 | "semi": [2, "always"], 7 | "strict": [2, "global"], 8 | "curly": 2, 9 | "eqeqeq": 2, 10 | "no-eval": 2, 11 | "guard-for-in": 2, 12 | "no-caller": 2, 13 | "no-else-return": 2, 14 | "no-eq-null": 2, 15 | "no-extend-native": 2, 16 | "no-extra-bind": 2, 17 | "no-floating-decimal": 2, 18 | "no-implied-eval": 2, 19 | "no-labels": 2, 20 | "no-with": 2, 21 | "no-loop-func": 1, 22 | "no-native-reassign": 2, 23 | "no-redeclare": [2, {"builtinGlobals": true}], 24 | "no-delete-var": 2, 25 | "no-shadow-restricted-names": 2, 26 | "no-undef-init": 2, 27 | "no-use-before-define": 2, 28 | "no-unused-vars": [2, {"args": "none"}], 29 | "no-undef": 2, 30 | "callback-return": [2, ["callback", "cb", "next"]], 31 | "global-require": 0, 32 | "no-console": 0, 33 | "generator-star-spacing": ["error", "after"], 34 | "require-yield": 0 35 | }, 36 | "env": { 37 | "es6": true, 38 | "node": true, 39 | "browser": true 40 | }, 41 | "globals": { 42 | "describe": true, 43 | "it": true, 44 | "before": true, 45 | "after": true 46 | }, 47 | "extends": "eslint:recommended" 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example 3 | .DS_Store 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "8" 6 | script: make test-coveralls 7 | -------------------------------------------------------------------------------- /MIT-License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jackson Tian 2 | Copyright (c) 2014-2015 Jackson Tian 3 | http://weibo.com/shyvo 4 | 5 | The MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 20000 4 | ISTANBUL = ./node_modules/.bin/istanbul 5 | ESLINT = ./node_modules/.bin/eslint 6 | MOCHA = ./node_modules/mocha/bin/_mocha 7 | COVERALLS = ./node_modules/coveralls/bin/coveralls.js 8 | 9 | lint: 10 | @$(ESLINT) --fix lib 11 | 12 | test: 13 | @NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 14 | $(MOCHA_OPTS) \ 15 | $(TESTS) 16 | 17 | test-cov: 18 | @$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 19 | 20 | test-coveralls: lint 21 | @$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 22 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 23 | @cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage 24 | 25 | test-all: test test-coveralls 26 | 27 | .PHONY: test 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wechat-oauth 2 | =============== 3 | 4 | 微信公共平台OAuth接口消息接口服务中间件与API SDK 5 | 6 | ## 模块状态 7 | 8 | - [![NPM version](https://badge.fury.io/js/wechat-oauth.png)](http://badge.fury.io/js/wechat-oauth) 9 | - [![Build Status](https://travis-ci.org/node-webot/wechat-oauth.png?branch=master)](https://travis-ci.org/node-webot/wechat-oauth) 10 | - [![Dependencies Status](https://david-dm.org/node-webot/wechat-oauth.png)](https://david-dm.org/node-webot/wechat-oauth) 11 | - [![Coverage Status](https://coveralls.io/repos/node-webot/wechat-oauth/badge.png)](https://coveralls.io/r/node-webot/wechat-oauth) 12 | 13 | ## 功能列表 14 | - OAuth授权 15 | - 获取基本信息 16 | 17 | OAuth2.0网页授权,使用此接口须通过微信认证,如果用户在微信中(Web微信除外)访问公众号的第三方网页,公众号开发者可以通过此接口获取当前用户基本信息(包括昵称、性别、城市、国家)。详见:[官方文档](http://mp.weixin.qq.com/wiki/index.php?title=网页授权获取用户基本信息) 18 | 19 | 详细参见[API文档](http://doxmate.cool/node-webot/wechat-oauth/api.html) 20 | 21 | ## Installation 22 | 23 | ```sh 24 | $ npm install wechat-oauth 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### 初始化 30 | 引入OAuth并实例化 31 | 32 | ```js 33 | var OAuth = require('wechat-oauth'); 34 | var client = new OAuth('your appid', 'your secret'); 35 | ``` 36 | 37 | 以上即可满足单进程使用。 38 | 当多进程时,token需要全局维护,以下为保存token的接口。 39 | 40 | ```js 41 | var oauthApi = new OAuth('appid', 'secret', function (openid, callback) { 42 | // 传入一个根据openid获取对应的全局token的方法 43 | // 在getUser时会通过该方法来获取token 44 | fs.readFile(openid +':access_token.txt', 'utf8', function (err, txt) { 45 | if (err) {return callback(err);} 46 | callback(null, JSON.parse(txt)); 47 | }); 48 | }, function (openid, token, callback) { 49 | // 请将token存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis等 50 | // 这样才能在cluster模式及多机情况下使用,以下为写入到文件的示例 51 | // 持久化时请注意,每个openid都对应一个唯一的token! 52 | fs.writeFile(openid + ':access_token.txt', JSON.stringify(token), callback); 53 | }); 54 | ``` 55 | 56 | 附上全局维护AccessToken的示例代码: 57 | 58 | Mongodb|mongoose 59 | 60 | ``` js 61 | var TokenSchema = new Schema({ 62 | access_token: String, 63 | expires_in: Number, 64 | refresh_token: String, 65 | openid: String, 66 | scope: String, 67 | create_at: String 68 | }); 69 | ``` 70 | 71 | 自定义getToken方法 72 | 73 | ```js 74 | TokenSchema.statics.getToken = function (openid, cb) { 75 | this.findOne({openid:openid}, function (err, result) { 76 | if (err) throw err; 77 | return cb(null, result); 78 | }); 79 | }; 80 | ``` 81 | 82 | 自定义saveToken方法 83 | 84 | ```js 85 | TokenSchema.statics.setToken = function (openid, token, cb) { 86 | // 有则更新,无则添加 87 | var query = {openid: openid}; 88 | var options = {upsert: true}; 89 | this.update(query, token, options, function (err, result) { 90 | if (err) throw err; 91 | return cb(null); 92 | }); 93 | }; 94 | 95 | mongoose.model('Token', 'TokenSchema'); 96 | ``` 97 | 98 | 初始化: 99 | 100 | ```js 101 | var client = new OAuth(appid, secret, function (openid, callback) { 102 | // 传入一个根据openid获取对应的全局token的方法 103 | // 在getUser时会通过该方法来获取token 104 | Token.getToken(openid, callback); 105 | }, function (openid, token, callback) { 106 | // 持久化时请注意,每个openid都对应一个唯一的token! 107 | Token.setToken(openid, token, callback); 108 | }); 109 | ``` 110 | 111 | MySQL: 112 | 113 | 建表SQL 114 | 115 | ```sql 116 | CREATE TABLE `token` ( 117 | `access_token` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '令牌', 118 | `expires_in` varchar(10) COLLATE utf8_bin NOT NULL COMMENT '有效期', 119 | `refresh_token` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '刷新参数', 120 | `openid` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '用户编号', 121 | `scope` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '作用域', 122 | `create_at` varchar(20) COLLATE utf8_bin NOT NULL COMMENT '令牌建立时间' 123 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='微信令牌表'; 124 | ``` 125 | 126 | 设置openid为唯一索引 127 | 128 | ```sql 129 | ALTER TABLE `token` 130 | ADD UNIQUE KEY `openid` (`openid`); 131 | ``` 132 | 133 | 使用示例: 134 | 135 | ```js 136 | var client = new Oauth(appid, secret, function (openid, callback) { 137 | var sql = 'SELECT * FROM token WHERE openid = ?'; 138 | db.query(sql, [openid], function (err, result) { 139 | if(err) { 140 | return callback(err); 141 | } 142 | return callback(null, result[0]); 143 | }); 144 | }, function (openid, token, callback) { 145 | var sql = 'REPLACE INTO token(access_token, expires_in, refresh_token, openid, scope, create_at) VALUES(?, ?, ?, ?, ?, ?)'; 146 | var fields = [token.access_token, token.expires_in, token.refresh_token, token.openid, token.scope, token.create_at]; 147 | db.query(sql, fields, function (err, result) { 148 | return callback(err); 149 | }); 150 | }); 151 | ``` 152 | 153 | ### 小程序初始化 154 | 使用小程序时,需要在初始化OAuth时指定`isMiniProgram`参数为`true` 155 | 156 | 单进程 157 | 158 | ```js 159 | var OAuth = require('wechat-oauth'); 160 | var client = new OAuth('your appid', 'your secret', null, null, true); // 最后一个参数即isMiniProgram 161 | ``` 162 | 163 | 多进程 164 | 165 | ```js 166 | var oauthApi = new OAuth('appid', 'secret', getToken, saveToken, true); 167 | ``` 168 | 169 | 注意:微信不会将用户的sessionKey过期时间告知开发者,该时间会根据用户与小程序互动频繁程度等因素发生变化,建议根据小程序客户端`wx.checkSession()`方法检验凭证是否依旧有效,若失效应该再次使用code换取新的sessionKey。故而此例中的`getToken`和`saveToken`方法过期机制须有不同。 170 | [官方文档](https://developers.weixin.qq.com/miniprogram/dev/api/signature.html) 171 | 172 | ### 引导用户 173 | 生成引导用户点击的URL。 174 | 175 | ```js 176 | var url = client.getAuthorizeURL('redirectUrl', 'state', 'scope'); 177 | ``` 178 | 179 | 如果是PC上的网页,请使用以下方式生成 180 | ```js 181 | var url = client.getAuthorizeURLForWebsite('redirectUrl'); 182 | ``` 183 | 184 | ### 获取Openid和AccessToken 185 | 用户点击上步生成的URL后会被重定向到上步设置的 `redirectUrl`,并且会带有`code`参数,我们可以使用这个`code`换取`access_token`和用户的`openid` 186 | 187 | ```js 188 | client.getAccessToken('code', function (err, result) { 189 | var accessToken = result.data.access_token; 190 | var openid = result.data.openid; 191 | }); 192 | ``` 193 | 194 | ### 获取用户信息 195 | 如果我们生成引导用户点击的URL中`scope`参数值为`snsapi_userinfo`,接下来我们就可以使用`openid`换取用户详细信息(必须在getAccessToken方法执行完成之后) 196 | 197 | ```js 198 | client.getUser(openid, function (err, result) { 199 | var userInfo = result; 200 | }); 201 | ``` 202 | 203 | ## 捐赠 204 | 如果您觉得Wechat OAuth对您有帮助,欢迎请作者一杯咖啡 205 | 206 | ![捐赠wechat](https://cloud.githubusercontent.com/assets/327019/2941591/2b9e5e58-d9a7-11e3-9e80-c25aba0a48a1.png) 207 | 208 | ## 交流群 209 | QQ群:157964097,使用疑问,开发,贡献代码请加群。 210 | 211 | ## Contributors 212 | 感谢以下贡献者: 213 | 214 | ``` 215 | $ git summary 216 | 217 | project : wechat-oauth 218 | repo age : 2 years, 2 months 219 | active : 13 days 220 | commits : 29 221 | files : 11 222 | authors : 223 | 24 Jackson Tian 82.8% 224 | 1 Kainy Guo 3.4% 225 | 1 Teng Fei 3.4% 226 | 1 cherry-geqi 3.4% 227 | 1 welch 3.4% 228 | 1 wzw 3.4% 229 | 230 | ``` 231 | 232 | ## License 233 | The MIT license. 234 | -------------------------------------------------------------------------------- /lib/oauth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var urllib = require('urllib'); 4 | var extend = require('util')._extend; 5 | var querystring = require('querystring'); 6 | 7 | var wrapper = require('./util').wrapper; 8 | var WxBizDataCrypt = require('./wx_biz_data_crypt'); 9 | 10 | var AccessToken = function (data) { 11 | if (!(this instanceof AccessToken)) { 12 | return new AccessToken(data); 13 | } 14 | this.data = data; 15 | }; 16 | 17 | /*! 18 | * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比 19 | * 20 | * Examples: 21 | * ``` 22 | * token.isValid(); 23 | * ``` 24 | */ 25 | AccessToken.prototype.isValid = function () { 26 | return !!this.data.access_token && (new Date().getTime()) < (this.data.create_at + this.data.expires_in * 1000); 27 | }; 28 | 29 | /*! 30 | * 处理token,更新过期时间 31 | */ 32 | var processToken = function (that, callback) { 33 | var create_at = new Date().getTime(); 34 | 35 | return function (err, data, res) { 36 | if (err) { 37 | return callback(err, data); 38 | } 39 | data.create_at = create_at; 40 | // 存储token 41 | that.saveToken(data.openid, data, function (err) { 42 | callback(err, new AccessToken(data)); 43 | }); 44 | }; 45 | }; 46 | 47 | /** 48 | * 根据appid和appsecret创建OAuth接口的构造函数 49 | * 如需跨进程跨机器进行操作,access token需要进行全局维护 50 | * 使用使用token的优先级是: 51 | * 52 | * 1. 使用当前缓存的token对象 53 | * 2. 调用开发传入的获取token的异步方法,获得token之后使用(并缓存它)。 54 | 55 | * Examples: 56 | * ``` 57 | * var OAuth = require('wechat-oauth'); 58 | * var api = new OAuth('appid', 'secret'); 59 | * ``` 60 | * @param {String} appid 在公众平台上申请得到的appid 61 | * @param {String} appsecret 在公众平台上申请得到的app secret 62 | * @param {Function} getToken 用于获取token的方法 63 | * @param {Function} saveToken 用于保存token的方法 64 | */ 65 | var OAuth = function (appid, appsecret, getToken, saveToken, isMiniProgram) { 66 | this.appid = appid; 67 | this.appsecret = appsecret; 68 | this.isMiniProgram = isMiniProgram; 69 | // token的获取和存储 70 | this.store = {}; 71 | this.getToken = getToken || function (openid, callback) { 72 | callback(null, this.store[openid]); 73 | }; 74 | if (!saveToken && process.env.NODE_ENV === 'production') { 75 | console.warn('Please dont save oauth token into memory under production'); 76 | } 77 | this.saveToken = saveToken || function (openid, token, callback) { 78 | this.store[openid] = token; 79 | callback(null); 80 | }; 81 | this.defaults = {}; 82 | }; 83 | 84 | /** 85 | * 用于设置urllib的默认options 86 | * 87 | * Examples: 88 | * ``` 89 | * oauth.setOpts({timeout: 15000}); 90 | * ``` 91 | * @param {Object} opts 默认选项 92 | */ 93 | OAuth.prototype.setOpts = function (opts) { 94 | this.defaults = opts; 95 | }; 96 | 97 | /*! 98 | * urllib的封装 99 | * 100 | * @param {String} url 路径 101 | * @param {Object} opts urllib选项 102 | * @param {Function} callback 回调函数 103 | */ 104 | OAuth.prototype.request = function (url, opts, callback) { 105 | var options = {}; 106 | extend(options, this.defaults); 107 | if (typeof opts === 'function') { 108 | callback = opts; 109 | opts = {}; 110 | } 111 | for (var key in opts) { 112 | if (key !== 'headers') { 113 | options[key] = opts[key]; 114 | } else { 115 | if (opts.headers) { 116 | options.headers = options.headers || {}; 117 | extend(options.headers, opts.headers); 118 | } 119 | } 120 | } 121 | urllib.request(url, options, callback); 122 | }; 123 | 124 | /** 125 | * 获取授权页面的URL地址 126 | * @param {String} redirect 授权后要跳转的地址 127 | * @param {String} state 开发者可提供的数据 128 | * @param {String} scope 作用范围,值为snsapi_userinfo和snsapi_base,前者用于弹出,后者用于跳转 129 | */ 130 | OAuth.prototype.getAuthorizeURL = function (redirect, state, scope) { 131 | var url = 'https://open.weixin.qq.com/connect/oauth2/authorize'; 132 | var info = { 133 | appid: this.appid, 134 | redirect_uri: redirect, 135 | response_type: 'code', 136 | scope: scope || 'snsapi_base', 137 | state: state || '' 138 | }; 139 | 140 | return url + '?' + querystring.stringify(info) + '#wechat_redirect'; 141 | }; 142 | 143 | /** 144 | * 获取授权页面的URL地址 145 | * @param {String} redirect 授权后要跳转的地址 146 | * @param {String} state 开发者可提供的数据 147 | * @param {String} scope 作用范围,值为snsapi_login,前者用于弹出,后者用于跳转 148 | */ 149 | OAuth.prototype.getAuthorizeURLForWebsite = function (redirect, state, scope) { 150 | var url = 'https://open.weixin.qq.com/connect/qrconnect'; 151 | var info = { 152 | appid: this.appid, 153 | redirect_uri: redirect, 154 | response_type: 'code', 155 | scope: scope || 'snsapi_login', 156 | state: state || '' 157 | }; 158 | 159 | return url + '?' + querystring.stringify(info) + '#wechat_redirect'; 160 | }; 161 | 162 | /** 163 | * 根据授权获取到的code,换取access token和openid 164 | * 获取openid之后,可以调用`wechat.API`来获取更多信息 165 | * Examples: 166 | * ``` 167 | * api.getAccessToken(code, callback); 168 | * ``` 169 | * Callback: 170 | * 171 | * - `err`, 获取access token出现异常时的异常对象 172 | * - `result`, 成功时得到的响应结果 173 | * 174 | * Result: 175 | * ``` 176 | * { 177 | * data: { 178 | * "access_token": "ACCESS_TOKEN", 179 | * "expires_in": 7200, 180 | * "refresh_token": "REFRESH_TOKEN", 181 | * "openid": "OPENID", 182 | * "scope": "SCOPE" 183 | * } 184 | * } 185 | * ``` 186 | * @param {String} code 授权获取到的code 187 | * @param {Function} callback 回调函数 188 | */ 189 | OAuth.prototype.getAccessToken = function (code, callback) { 190 | var url = 'https://api.weixin.qq.com/sns/oauth2/access_token'; 191 | var info = { 192 | appid: this.appid, 193 | secret: this.appsecret, 194 | code: code, 195 | grant_type: 'authorization_code' 196 | }; 197 | var args = { 198 | data: info, 199 | dataType: 'json' 200 | }; 201 | this.request(url, args, wrapper(processToken(this, callback))); 202 | }; 203 | 204 | /** 205 | * 根据授权获取到的code,换取小程序的session key和openid(以及有条件下的unionid) 206 | * 获取openid之后,可以调用`wechat.API`来获取更多信息 207 | * Examples: 208 | * ``` 209 | * api.getSessionKey(code, callback); 210 | * ``` 211 | * Callback: 212 | * 213 | * - `err`, 获取session key出现异常时的异常对象 214 | * - `result`, 成功时得到的响应结果 215 | * 216 | * Result: 217 | * ``` 218 | * { 219 | * data: { 220 | * "session_key": "SESSION_KEY", 221 | * "openid": "OPENID", 222 | * "unionid": "UNIONID" 223 | * } 224 | * } 225 | * ``` 226 | * @param {String} code 授权获取到的code 227 | * @param {Function} callback 回调函数 228 | */ 229 | OAuth.prototype.getSessionKey = function(code, callback) { 230 | var url = 'https://api.weixin.qq.com/sns/jscode2session'; 231 | var info = { 232 | appid: this.appid, 233 | secret: this.appsecret, 234 | js_code: code, 235 | grant_type: 'authorization_code', 236 | }; 237 | var args = { 238 | data: info, 239 | dataType: 'json' 240 | }; 241 | this.request(url, args, wrapper(processToken(this, callback))); 242 | }; 243 | 244 | /** 245 | * 根据refresh token,刷新access token,调用getAccessToken后才有效 246 | * Examples: 247 | * ``` 248 | * api.refreshAccessToken(refreshToken, callback); 249 | * ``` 250 | * Callback: 251 | * 252 | * - `err`, 刷新access token出现异常时的异常对象 253 | * - `result`, 成功时得到的响应结果 254 | * 255 | * Result: 256 | * ``` 257 | * { 258 | * data: { 259 | * "access_token": "ACCESS_TOKEN", 260 | * "expires_in": 7200, 261 | * "refresh_token": "REFRESH_TOKEN", 262 | * "openid": "OPENID", 263 | * "scope": "SCOPE" 264 | * } 265 | * } 266 | * ``` 267 | * @param {String} refreshToken refreshToken 268 | * @param {Function} callback 回调函数 269 | */ 270 | OAuth.prototype.refreshAccessToken = function (refreshToken, callback) { 271 | var url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token'; 272 | var info = { 273 | appid: this.appid, 274 | grant_type: 'refresh_token', 275 | refresh_token: refreshToken 276 | }; 277 | var args = { 278 | data: info, 279 | dataType: 'json' 280 | }; 281 | this.request(url, args, wrapper(processToken(this, callback))); 282 | }; 283 | 284 | OAuth.prototype._getUser = function (options, accessToken, callback) { 285 | var url = 'https://api.weixin.qq.com/sns/userinfo'; 286 | var info = { 287 | access_token: accessToken, 288 | openid: options.openid, 289 | lang: options.lang || 'en' 290 | }; 291 | var args = { 292 | data: info, 293 | dataType: 'json' 294 | }; 295 | this.request(url, args, wrapper(callback)); 296 | }; 297 | 298 | /** 299 | * 根据服务器保存的sessionKey对从小程序客户端获取的加密用户数据进行解密 300 | * Examples: 301 | * ``` 302 | * api.decryptMiniProgramUser({encryptedData, iv}, callback); 303 | * ``` 304 | * Callback: 305 | * 306 | * - `err`, 解密用户信息出现异常时的异常对象 307 | * - `result`, 成功时得到的响应结果 308 | * 309 | * Result: 310 | * ``` 311 | *{ 312 | * "openId": "OPENID", 313 | * "nickName": "NICKNAME", 314 | * "gender": "GENDER", 315 | * "city": "CITY", 316 | * "province": "PROVINCE", 317 | * "country": "COUNTRY", 318 | * "avatarUrl": "AVATARURL", 319 | * "unionId": "UNIONID", 320 | * "watermark": 321 | * { 322 | * "appid":"APPID", 323 | * "timestamp":TIMESTAMP 324 | * } 325 | *} 326 | * ``` 327 | * @param {Object} options 需要解密的对象 328 | * @param {String} options.encryptedData 从小程序中获得的加密过的字符串 329 | * @param {String} options.iv 从小程序中获得的加密算法初始向量 330 | */ 331 | OAuth.prototype.decryptMiniProgramUser = function (options) { 332 | var decrypter = new WxBizDataCrypt(this.appid, options.sessionKey); 333 | return decrypter.decryptData(options.encryptedData, options.iv); 334 | }; 335 | 336 | /** 337 | * 根据openid,获取用户信息。 338 | * 当access token无效时,自动通过refresh token获取新的access token。然后再获取用户信息 339 | * Examples: 340 | * ``` 341 | * api.getUser(openid, callback); 342 | * api.getUser(options, callback); 343 | * ``` 344 | * 345 | * Options: 346 | * ``` 347 | * // 或 348 | * { 349 | * "openid": "the open Id", // 必须 350 | * "lang": "the lang code" // zh_CN 简体,zh_TW 繁体,en 英语 351 | * } 352 | * ``` 353 | * Callback: 354 | * 355 | * - `err`, 获取用户信息出现异常时的异常对象 356 | * - `result`, 成功时得到的响应结果 357 | * 358 | * Result: 359 | * ``` 360 | * { 361 | * "openid": "OPENID", 362 | * "nickname": "NICKNAME", 363 | * "sex": "1", 364 | * "province": "PROVINCE" 365 | * "city": "CITY", 366 | * "country": "COUNTRY", 367 | * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", 368 | * "privilege": [ 369 | * "PRIVILEGE1" 370 | * "PRIVILEGE2" 371 | * ] 372 | * } 373 | * ``` 374 | * @param {Object|String} options 传入openid或者参见Options 375 | * @param {Function} callback 回调函数 376 | */ 377 | OAuth.prototype.getUser = function (options, callback) { 378 | if (typeof options !== 'object') { 379 | options = { 380 | openid: options 381 | }; 382 | } 383 | var that = this; 384 | this.getToken(options.openid, function (err, data) { 385 | if (err) { 386 | return callback(err); 387 | } 388 | // 没有token数据 389 | if (!data) { 390 | var error = new Error('No token for ' + options.openid + ', please authorize first.'); 391 | error.name = 'NoOAuthTokenError'; 392 | return callback(error); 393 | } 394 | var token = new AccessToken(data); 395 | if (token.isValid()) { 396 | that._getUser(options, token.data.access_token, callback); 397 | } else { 398 | that.refreshAccessToken(token.data.refresh_token, function (err, token) { 399 | if (err) { 400 | return callback(err); 401 | } 402 | that._getUser(options, token.data.access_token, callback); 403 | }); 404 | } 405 | }); 406 | }; 407 | 408 | /** 409 | * 检验授权凭证(access_token)是否有效。 410 | * Examples: 411 | * ``` 412 | * api.verifyToken(openid, accessToken, callback); 413 | * ``` 414 | * @param {String} openid 传入openid 415 | * @param {String} accessToken 待校验的access token 416 | * @param {Function} callback 回调函数 417 | */ 418 | OAuth.prototype.verifyToken = function (openid, accessToken, callback) { 419 | var url = 'https://api.weixin.qq.com/sns/auth'; 420 | var info = { 421 | access_token: accessToken, 422 | openid: openid 423 | }; 424 | var args = { 425 | data: info, 426 | dataType: 'json' 427 | }; 428 | this.request(url, args, wrapper(callback)); 429 | }; 430 | 431 | /** 432 | * 根据code,获取用户信息。注意,当OAuth为MiniProgram类型时,返回的用户对象会有所不同,请查看官方文档确定数据结构以便解析。 433 | * Examples: 434 | * ``` 435 | * api.getUserByCode(code, callback); 436 | * ``` 437 | * Callback: 438 | * 439 | * - `err`, 获取用户信息出现异常时的异常对象 440 | * - `result`, 成功时得到的响应结果 441 | * 442 | * Result: 443 | * ``` 444 | * { 445 | * "openid": "OPENID", 446 | * "nickname": "NICKNAME", 447 | * "sex": "1", 448 | * "province": "PROVINCE" 449 | * "city": "CITY", 450 | * "country": "COUNTRY", 451 | * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", 452 | * "privilege": [ 453 | * "PRIVILEGE1" 454 | * "PRIVILEGE2" 455 | * ] 456 | * } 457 | * ``` 458 | * @param {Object|String} options 授权获取到的code 459 | * @param {Function} callback 回调函数 460 | */ 461 | OAuth.prototype.getUserByCode = function (options, callback) { 462 | var that = this; 463 | 464 | var lang, code; 465 | if (typeof options === 'string') { 466 | code = options; 467 | } else { 468 | lang = options.lang; 469 | code = options.code; 470 | } 471 | 472 | if (this.isMiniProgram) { 473 | this.getSessionKey(code, function (err, result) { 474 | if (err) { 475 | return callback(err); 476 | } 477 | var sessionKey = result.data.session_key; 478 | var user; 479 | try { 480 | user = that.decryptMiniProgramUser({ 481 | sessionKey, 482 | encryptedData: options.encryptedData, 483 | iv: options.iv, 484 | }); 485 | } catch (ex) { 486 | return callback(ex); 487 | } 488 | 489 | callback(null, user); 490 | }); 491 | } else { 492 | this.getAccessToken(code, function (err, result) { 493 | if (err) { 494 | return callback(err); 495 | } 496 | var openid = result.data.openid; 497 | that.getUser({openid: openid, lang: lang}, callback); 498 | }); 499 | } 500 | }; 501 | 502 | module.exports = OAuth; 503 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 5 | * 参见:http://mp.weixin.qq.com/wiki/index.php?title=返回码说明 6 | */ 7 | exports.wrapper = function (callback) { 8 | return function (err, data, res) { 9 | callback = callback || function () {}; 10 | if (err) { 11 | err.name = 'WeChatAPI' + err.name; 12 | return callback(err, data, res); 13 | } 14 | if (data.errcode) { 15 | err = new Error(data.errmsg); 16 | err.name = 'WeChatAPIError'; 17 | err.code = data.errcode; 18 | return callback(err, data, res); 19 | } 20 | callback(null, data, res); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/wx_biz_data_crypt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | 5 | /** 6 | * 根据appId和小程序的sessionKey对小程序解密器的构造函数 7 | * 该代码来自官方示例:https://developers.weixin.qq.com/miniprogram/dev/api/signature.html 8 | * Examples: 9 | * ``` 10 | * var WXBizDataCrypt = require('./wx_biz_data_crypt'); 11 | * var decrypter = new WXBizDataCrypt('appid', 'sessionKey'); 12 | * ``` 13 | * @param {String} appid 在公众平台上申请得到的appid 14 | * @param {String} session_key 根据appid和小程序auth code获得的对应用户sessionKey 15 | */ 16 | function WXBizDataCrypt(appId, sessionKey) { 17 | this.appId = appId; 18 | this.sessionKey = sessionKey; 19 | } 20 | 21 | /** 22 | * 通过已有的解密器对小程序加密数据进行解密 23 | * 24 | * @param {String} encryptedData 从小程序中获得的加密数据,格式应该为base64 25 | * @param {String} iv 从小程序中获得加密算法初始向量initial-vector,格式应当为base64 26 | */ 27 | WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) { 28 | // base64 decode 29 | var sessionKey = new Buffer(this.sessionKey, 'base64'); 30 | var encryptedBuffer = new Buffer(encryptedData, 'base64'); 31 | var ivBuffer = new Buffer(iv, 'base64'); 32 | 33 | try { 34 | // 解密 35 | var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, ivBuffer); 36 | // 设置自动 padding 为 true,删除填充补位 37 | decipher.setAutoPadding(true); 38 | var decoded = decipher.update(encryptedBuffer, 'binary', 'utf8'); 39 | decoded += decipher.final('utf8'); 40 | 41 | decoded = JSON.parse(decoded); 42 | 43 | } catch (err) { 44 | throw new Error('Illegal Buffer, Is Your Data Correct?'); 45 | } 46 | 47 | if (decoded.watermark.appid !== this.appId) { 48 | throw new Error('Invalid Watermark, Be Sure to Check Again'); 49 | } 50 | 51 | return decoded; 52 | }; 53 | 54 | module.exports = WXBizDataCrypt; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-oauth", 3 | "version": "1.5.0", 4 | "description": "微信公共平台OAuth", 5 | "main": "lib/oauth.js", 6 | "scripts": { 7 | "test": "make test-all" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/node-webot/wechat-oauth.git" 12 | }, 13 | "keywords": [ 14 | "weixin", 15 | "wechat" 16 | ], 17 | "dependencies": { 18 | "urllib": "^2.30.0" 19 | }, 20 | "devDependencies": { 21 | "coveralls": "*", 22 | "eslint": "^4.19.1", 23 | "expect.js": "*", 24 | "istanbul": "*", 25 | "mocha": "*", 26 | "mocha-lcov-reporter": "*", 27 | "muk": "*", 28 | "rewire": "*", 29 | "supertest": "*" 30 | }, 31 | "author": "Jackson Tian", 32 | "license": "MIT", 33 | "readmeFilename": "README.md", 34 | "directories": { 35 | "test": "test" 36 | }, 37 | "files": [ 38 | "lib" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | appid: 'wxc9135aade4e81d57', 3 | appsecret: '0461795e98b8ffde5a212b5098f1b9b6' 4 | }; 5 | -------------------------------------------------------------------------------- /test/oauth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect.js'); 4 | var urllib = require('urllib'); 5 | var muk = require('muk'); 6 | var OAuth = require('../'); 7 | var config = require('./config'); 8 | 9 | describe('oauth.js', function () { 10 | describe('getAuthorizeURL', function () { 11 | var auth = new OAuth('appid', 'appsecret'); 12 | it('should ok', function () { 13 | var url = auth.getAuthorizeURL('http://diveintonode.org/'); 14 | expect(url).to.be.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=http%3A%2F%2Fdiveintonode.org%2F&response_type=code&scope=snsapi_base&state=#wechat_redirect'); 15 | }); 16 | 17 | it('should ok with state', function () { 18 | var url = auth.getAuthorizeURL('http://diveintonode.org/', 'hehe'); 19 | expect(url).to.be.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=http%3A%2F%2Fdiveintonode.org%2F&response_type=code&scope=snsapi_base&state=hehe#wechat_redirect'); 20 | }); 21 | 22 | it('should ok with state and scope', function () { 23 | var url = auth.getAuthorizeURL('http://diveintonode.org/', 'hehe', 'snsapi_userinfo'); 24 | expect(url).to.be.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=http%3A%2F%2Fdiveintonode.org%2F&response_type=code&scope=snsapi_userinfo&state=hehe#wechat_redirect'); 25 | }); 26 | }); 27 | 28 | describe('getAuthorizeURLForWebsite', function () { 29 | var auth = new OAuth('appid', 'appsecret'); 30 | it('should ok', function () { 31 | var url = auth.getAuthorizeURLForWebsite('http://diveintonode.org/'); 32 | expect(url).to.be.equal('https://open.weixin.qq.com/connect/qrconnect?appid=appid&redirect_uri=http%3A%2F%2Fdiveintonode.org%2F&response_type=code&scope=snsapi_login&state=#wechat_redirect'); 33 | }); 34 | 35 | it('should ok with state', function () { 36 | var url = auth.getAuthorizeURLForWebsite('http://diveintonode.org/', 'hehe'); 37 | expect(url).to.be.equal('https://open.weixin.qq.com/connect/qrconnect?appid=appid&redirect_uri=http%3A%2F%2Fdiveintonode.org%2F&response_type=code&scope=snsapi_login&state=hehe#wechat_redirect'); 38 | }); 39 | 40 | it('should ok with state and scope', function () { 41 | var url = auth.getAuthorizeURLForWebsite('http://diveintonode.org/', 'hehe', 'snsapi_userinfo'); 42 | expect(url).to.be.equal('https://open.weixin.qq.com/connect/qrconnect?appid=appid&redirect_uri=http%3A%2F%2Fdiveintonode.org%2F&response_type=code&scope=snsapi_userinfo&state=hehe#wechat_redirect'); 43 | }); 44 | }); 45 | 46 | describe('getAccessToken', function () { 47 | var api = new OAuth(config.appid, config.appsecret); 48 | it('should invalid', function (done) { 49 | api.getAccessToken('code', function (err, data) { 50 | expect(err).to.be.ok(); 51 | expect(err.name).to.be.equal('WeChatAPIError'); 52 | expect(err.message).to.contain('invalid code'); 53 | done(); 54 | }); 55 | }); 56 | 57 | describe('should ok', function () { 58 | before(function () { 59 | muk(urllib, 'request', function (url, args, callback) { 60 | var resp = { 61 | "access_token":"ACCESS_TOKEN", 62 | "expires_in":7200, 63 | "refresh_token":"REFRESH_TOKEN", 64 | "openid":"OPENID", 65 | "scope":"SCOPE" 66 | }; 67 | process.nextTick(function () { 68 | callback(null, resp); 69 | }); 70 | }); 71 | }); 72 | 73 | after(function () { 74 | muk.restore(); 75 | }); 76 | 77 | it('should ok', function (done) { 78 | api.getAccessToken('code', function (err, token) { 79 | expect(err).not.to.be.ok(); 80 | expect(token).to.have.property('data'); 81 | expect(token.data).to.have.keys('access_token', 'expires_in', 'refresh_token', 'openid', 'scope', 'create_at'); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('should not ok', function () { 88 | before(function () { 89 | muk(urllib, 'request', function (url, args, callback) { 90 | var resp = { 91 | "access_token":"ACCESS_TOKEN", 92 | "expires_in": 0.1, 93 | "refresh_token":"REFRESH_TOKEN", 94 | "openid":"OPENID", 95 | "scope":"SCOPE" 96 | }; 97 | 98 | setTimeout(function () { 99 | callback(null, resp); 100 | }, 100); 101 | }); 102 | }); 103 | 104 | after(function () { 105 | muk.restore(); 106 | }); 107 | 108 | it('should not ok', function (done) { 109 | api.getAccessToken('code', function (err, token) { 110 | expect(token.isValid()).not.to.be.ok(); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('refreshAccessToken', function () { 118 | var api = new OAuth('appid', 'secret'); 119 | 120 | it('should invalid', function (done) { 121 | api.refreshAccessToken('refresh_token', function (err, data) { 122 | expect(err).to.be.ok(); 123 | expect(err.name).to.be.equal('WeChatAPIError'); 124 | expect(err.message).to.contain('invalid appid'); 125 | done(); 126 | }); 127 | }); 128 | 129 | describe('should ok', function () { 130 | before(function () { 131 | muk(urllib, 'request', function (url, args, callback) { 132 | var resp = { 133 | "access_token":"ACCESS_TOKEN", 134 | "expires_in":7200, 135 | "refresh_token":"REFRESH_TOKEN", 136 | "openid":"OPENID", 137 | "scope":"SCOPE" 138 | }; 139 | process.nextTick(function () { 140 | callback(null, resp); 141 | }); 142 | }); 143 | }); 144 | 145 | after(function () { 146 | muk.restore(); 147 | }); 148 | 149 | it('should ok', function (done) { 150 | api.refreshAccessToken('refresh_token', function (err, token) { 151 | expect(err).not.to.be.ok(); 152 | expect(token.data).to.have.keys('access_token', 'expires_in', 'refresh_token', 'openid', 'scope', 'create_at'); 153 | done(); 154 | }); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('_getUser', function () { 160 | it('should invalid', function (done) { 161 | var api = new OAuth('appid', 'secret'); 162 | api._getUser('openid', 'access_token', function (err, data) { 163 | expect(err).to.be.ok(); 164 | expect(err.name).to.be.equal('WeChatAPIError'); 165 | expect(err.message).to.contain('invalid credential, access_token is invalid or not latest'); 166 | done(); 167 | }); 168 | }); 169 | 170 | describe('mock get user ok', function () { 171 | var api = new OAuth('appid', 'secret'); 172 | before(function () { 173 | muk(urllib, 'request', function (url, args, callback) { 174 | process.nextTick(function () { 175 | callback(null, { 176 | "openid": "OPENID", 177 | "nickname": "NICKNAME", 178 | "sex": "1", 179 | "province": "PROVINCE", 180 | "city": "CITY", 181 | "country": "COUNTRY", 182 | "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", 183 | "privilege": [ 184 | "PRIVILEGE1", 185 | "PRIVILEGE2" 186 | ] 187 | }); 188 | }); 189 | }); 190 | }); 191 | 192 | after(function () { 193 | muk.restore(); 194 | }); 195 | 196 | it('should ok', function (done) { 197 | api._getUser('openid', 'access_token', function (err, data) { 198 | expect(err).not.to.be.ok(); 199 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 200 | 'country', 'headimgurl', 'privilege'); 201 | done(); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | describe('getUser', function () { 208 | it('can not get token', function (done) { 209 | var api = new OAuth('appid', 'secret'); 210 | api.getUser('openid', function (err, data) { 211 | expect(err).to.be.ok(); 212 | expect(err.message).to.be.equal('No token for openid, please authorize first.'); 213 | done(); 214 | }); 215 | }); 216 | 217 | describe('mock get token error', function () { 218 | var api = new OAuth('appid', 'secret'); 219 | before(function () { 220 | muk(api, 'getToken', function (openid, callback) { 221 | process.nextTick(function () { 222 | callback(new Error('get token error')); 223 | }); 224 | }); 225 | }); 226 | 227 | after(function () { 228 | muk.restore(); 229 | }); 230 | 231 | it('should ok', function (done) { 232 | api.getUser('openid', function (err, data) { 233 | expect(err).to.be.ok(); 234 | expect(err.message).to.be.equal('get token error'); 235 | done(); 236 | }); 237 | }); 238 | }); 239 | 240 | describe('mock get null data', function () { 241 | var api = new OAuth('appid', 'secret'); 242 | before(function () { 243 | muk(api, 'getToken', function (openid, callback) { 244 | process.nextTick(function () { 245 | callback(null, null); 246 | }); 247 | }); 248 | }); 249 | 250 | after(function () { 251 | muk.restore(); 252 | }); 253 | 254 | it('should ok', function (done) { 255 | api.getUser('openid', function (err, data) { 256 | expect(err).to.be.ok(); 257 | expect(err).to.have.property('name', 'NoOAuthTokenError'); 258 | expect(err).to.have.property('message', 'No token for openid, please authorize first.'); 259 | done(); 260 | }); 261 | }); 262 | }); 263 | 264 | describe('mock get valid token', function () { 265 | var api = new OAuth('appid', 'secret'); 266 | before(function () { 267 | muk(api, 'getToken', function (openid, callback) { 268 | process.nextTick(function () { 269 | callback(null, { 270 | access_token: 'access_token', 271 | create_at: new Date().getTime(), 272 | expires_in: 60 273 | }); 274 | }); 275 | }); 276 | muk(api, '_getUser', function (openid, accessToken, callback) { 277 | process.nextTick(function () { 278 | callback(null, { 279 | "openid": "OPENID", 280 | "nickname": "NICKNAME", 281 | "sex": "1", 282 | "province": "PROVINCE", 283 | "city": "CITY", 284 | "country": "COUNTRY", 285 | "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", 286 | "privilege": [ 287 | "PRIVILEGE1", 288 | "PRIVILEGE2" 289 | ] 290 | }); 291 | }); 292 | }); 293 | }); 294 | 295 | after(function () { 296 | muk.restore(); 297 | }); 298 | 299 | it('should ok with openid', function (done) { 300 | api.getUser('openid', function (err, data) { 301 | expect(err).not.to.be.ok(); 302 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 303 | 'country', 'headimgurl', 'privilege'); 304 | done(); 305 | }); 306 | }); 307 | 308 | it('should ok with options', function (done) { 309 | api.getUser({openid: 'openid', lang: 'en'}, function (err, data) { 310 | expect(err).not.to.be.ok(); 311 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 312 | 'country', 'headimgurl', 'privilege'); 313 | done(); 314 | }); 315 | }); 316 | 317 | it('should ok with options', function (done) { 318 | api.getUser({openid: 'openid'}, function (err, data) { 319 | expect(err).not.to.be.ok(); 320 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 321 | 'country', 'headimgurl', 'privilege'); 322 | done(); 323 | }); 324 | }); 325 | }); 326 | 327 | describe('mock get invalid token', function () { 328 | var api = new OAuth('appid', 'secret'); 329 | before(function () { 330 | muk(api, 'getToken', function (openid, callback) { 331 | process.nextTick(function () { 332 | callback(null, { 333 | access_token: 'access_token', 334 | create_at: new Date().getTime() - 70 * 1000, 335 | expires_in: 60 336 | }); 337 | }); 338 | }); 339 | }); 340 | 341 | after(function () { 342 | muk.restore(); 343 | }); 344 | 345 | it('should ok', function (done) { 346 | api.getUser('openid', function (err, data) { 347 | expect(err).to.be.ok(); 348 | expect(err).to.have.property('name', 'WeChatAPIError'); 349 | expect(err.message).to.contain('refresh_token missing'); 350 | done(); 351 | }); 352 | }); 353 | }); 354 | 355 | describe('mock get invalid token and refresh_token', function () { 356 | var api = new OAuth('appid', 'secret'); 357 | before(function () { 358 | muk(api, 'getToken', function (openid, callback) { 359 | process.nextTick(function () { 360 | callback(null, { 361 | access_token: 'access_token', 362 | refresh_token: 'refresh_token', 363 | create_at: new Date().getTime() - 70 * 1000, 364 | expires_in: 60 365 | }); 366 | }); 367 | }); 368 | 369 | muk(api, 'refreshAccessToken', function (refreshToken, callback) { 370 | var resp = { 371 | data: { 372 | "access_token": "ACCESS_TOKEN", 373 | "expires_in": 7200, 374 | "refresh_token": "REFRESH_TOKEN", 375 | "openid": "OPENID", 376 | "scope": "SCOPE" 377 | } 378 | }; 379 | process.nextTick(function () { 380 | callback(null, resp); 381 | }); 382 | }); 383 | 384 | muk(api, '_getUser', function (openid, accessToken, callback) { 385 | process.nextTick(function () { 386 | callback(null, { 387 | "openid": "OPENID", 388 | "nickname": "NICKNAME", 389 | "sex": "1", 390 | "province": "PROVINCE", 391 | "city": "CITY", 392 | "country": "COUNTRY", 393 | "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", 394 | "privilege": [ 395 | "PRIVILEGE1", 396 | "PRIVILEGE2" 397 | ] 398 | }); 399 | }); 400 | }); 401 | }); 402 | 403 | after(function () { 404 | muk.restore(); 405 | }); 406 | 407 | it('should ok', function (done) { 408 | api.getUser('openid', function (err, data) { 409 | expect(err).not.to.be.ok(); 410 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 'country', 'headimgurl', 'privilege'); 411 | done(); 412 | }); 413 | }); 414 | }); 415 | }); 416 | 417 | describe('mock getUserByCode', function () { 418 | var api = new OAuth('appid', 'secret'); 419 | before(function () { 420 | muk(urllib, 'request', function (url, args, callback) { 421 | var resp = { 422 | "access_token":"ACCESS_TOKEN", 423 | "expires_in":7200, 424 | "refresh_token":"REFRESH_TOKEN", 425 | "openid":"OPENID", 426 | "scope":"SCOPE" 427 | }; 428 | process.nextTick(function () { 429 | callback(null, resp); 430 | }); 431 | }); 432 | 433 | muk(api, '_getUser', function (openid, accessToken, callback) { 434 | process.nextTick(function () { 435 | callback(null, { 436 | "openid": "OPENID", 437 | "nickname": "NICKNAME", 438 | "sex": "1", 439 | "province": "PROVINCE", 440 | "city": "CITY", 441 | "country": "COUNTRY", 442 | "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", 443 | "privilege": [ 444 | "PRIVILEGE1", 445 | "PRIVILEGE2" 446 | ] 447 | }); 448 | }); 449 | }); 450 | }); 451 | 452 | after(function () { 453 | muk.restore(); 454 | }); 455 | 456 | it('should ok with getUserByCode', function (done) { 457 | api.getUserByCode('code', function (err, data) { 458 | expect(err).not.to.be.ok(); 459 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 460 | 'country', 'headimgurl', 'privilege'); 461 | done(); 462 | }); 463 | }); 464 | 465 | it('should ok with getUserByCode', function (done) { 466 | var options = {code: 'code', lang: 'en'}; 467 | api.getUserByCode(options, function (err, data) { 468 | expect(err).not.to.be.ok(); 469 | expect(data).to.have.keys('openid', 'nickname', 'sex', 'province', 'city', 470 | 'country', 'headimgurl', 'privilege'); 471 | done(); 472 | }); 473 | }); 474 | }); 475 | 476 | describe('mock getUserByCode mini program', function () { 477 | describe('should ok', function () { 478 | var api = new OAuth('appid', 'secret', null, null, true); 479 | before(function () { 480 | muk(api, 'getSessionKey', function (code, callback) { 481 | var resp = { 482 | data: { 483 | session_key: 'SESSION_KEY', 484 | expires_in:7200, 485 | openid: 'OPENID', 486 | unionid: 'UNIONID' 487 | } 488 | }; 489 | process.nextTick(function () { 490 | callback(null, resp); 491 | }); 492 | }); 493 | 494 | muk(api, 'decryptMiniProgramUser', function (code) { 495 | return { 496 | openId: 'OPENID', 497 | nickName: 'NICKNAME', 498 | gender: 0, 499 | city: 'CITY', 500 | province: 'PROVINCE', 501 | country: 'COUNTRY', 502 | avatarUrl: 'AVATARURL', 503 | unionId: 'UNIONID', 504 | }; 505 | }); 506 | }); 507 | 508 | it('should ok with getUserByCode', function (done) { 509 | api.getUserByCode('code', function (err, data) { 510 | expect(err).not.to.be.ok(); 511 | expect(data).to.have.keys('openId', 'nickName', 'gender', 'province', 'city', 512 | 'country', 'avatarUrl'); 513 | done(); 514 | }); 515 | }); 516 | 517 | after(function () { 518 | muk.restore(); 519 | }); 520 | }); 521 | 522 | describe('should not ok', function () { 523 | it('should not ok if get session key throws an error', function (done) { 524 | var api = new OAuth('appid', 'secret', null, null, true); 525 | 526 | muk(api, 'getSessionKey', function (code, callback) { 527 | callback(new Error('mock error')); 528 | }); 529 | 530 | api.getUserByCode('code', function (err, data) { 531 | expect(err).to.be.a(Error); 532 | done(); 533 | }); 534 | 535 | muk.restore(); 536 | }); 537 | }); 538 | }); 539 | 540 | describe('verifyToken', function () { 541 | var api = new OAuth('appid', 'secret'); 542 | it('should ok with verifyToken', function (done) { 543 | api.verifyToken('openid', 'access_token', function (err, data) { 544 | expect(err).to.be.ok(); 545 | expect(err.message).to.contain('access_token is invalid'); 546 | done(); 547 | }); 548 | }); 549 | }); 550 | 551 | describe('getSessionKey', function () { 552 | var api = new OAuth('appid', 'secret', null, null, true); 553 | it('should invalid', function (done) { 554 | api.getSessionKey('code', function (err, result) { 555 | expect(err).to.be.ok(); 556 | expect(err.name).to.be.equal('WeChatAPIError'); 557 | expect(err.message).to.contain('invalid appid'); 558 | done(); 559 | }); 560 | }); 561 | 562 | describe('should ok', function () { 563 | before(function () { 564 | muk(urllib, 'request', function (url, args, callback) { 565 | var resp = { 566 | session_key: 'SESSION_KEY', 567 | expires_in:7200, 568 | openid: 'OPENID', 569 | unionid: 'UNIONID' 570 | }; 571 | process.nextTick(function () { 572 | callback(null, resp); 573 | }); 574 | }); 575 | }); 576 | 577 | after(function () { 578 | muk.restore(); 579 | }); 580 | 581 | it('should ok', function (done) { 582 | api.getSessionKey('code', function (err, token) { 583 | expect(err).not.to.be.ok(); 584 | expect(token).to.have.property('data'); 585 | expect(token.data).to.have.keys('session_key', 'openid', 'create_at'); 586 | done(); 587 | }); 588 | }); 589 | }); 590 | }); 591 | 592 | describe('decryptMiniProgramUser', function () { 593 | describe('should not ok', function () { 594 | var api = new OAuth('appid', 'secret', null, null, true); 595 | it('should not ok with invalid data', function () { 596 | expect(function () { 597 | api.decryptMiniProgramUser({}); 598 | }).to.throwError(); 599 | }); 600 | }); 601 | }); 602 | }); 603 | -------------------------------------------------------------------------------- /test/wx_biz_data_crypt.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | var expect = require('expect.js'); 5 | var urllib = require('urllib'); 6 | var WxBizCrypt = require('../lib/wx_biz_data_crypt'); 7 | 8 | describe('wx_biz_data_crypt.js', function () { 9 | describe('decryptData', function () { 10 | var appId = 'appId'; 11 | var sessionKey = crypto.randomBytes(16).toString('base64'); 12 | var iv = crypto.randomBytes(16).toString('base64'); 13 | var data = { 14 | openid: 'openid', 15 | exampleField1: 'just a example', 16 | watermark: { 17 | appid: appId, 18 | }, 19 | }; 20 | 21 | var cipher = crypto.createCipheriv('aes-128-cbc', new Buffer(sessionKey, 'base64'), new Buffer(iv, 'base64')); 22 | cipher.setAutoPadding(true); 23 | var encryptedData = cipher.update(JSON.stringify(data), 'utf8', 'base64'); 24 | encryptedData += cipher.final('base64'); 25 | 26 | var cryptor = new WxBizCrypt(appId, sessionKey); 27 | 28 | it('should ok', function () { 29 | var decryptedData = cryptor.decryptData(encryptedData, iv); 30 | expect(decryptedData.openid).to.equal(data.openid); 31 | expect(decryptedData.exampleField1).to.equal(data.exampleField1); 32 | expect(decryptedData.watermark.appid).to.equal(data.watermark.appid); 33 | }); 34 | 35 | it('should not ok on invalid decrypted data input', function (){ 36 | try{ 37 | cryptor.decryptData('', iv); 38 | }catch(e){ 39 | expect(e).to.be.a(Error); 40 | } 41 | }); 42 | 43 | it('should not ok on invalid decrypted data input', function (){ 44 | try{ 45 | cryptor.decryptData(encryptedData, ''); 46 | }catch(e){ 47 | expect(e).to.be.a(Error); 48 | } 49 | }); 50 | 51 | it('should not ok with invalid app id', function (){ 52 | var invalidCryptor = new WxBizCrypt('invalid app id', sessionKey); 53 | console.log(invalidCryptor.appId); 54 | try{ 55 | invalidCryptor.decryptData(encryptedData, iv); 56 | }catch(e){ 57 | expect(e).to.be.a(Error); 58 | } 59 | }); 60 | }); 61 | }); 62 | --------------------------------------------------------------------------------