├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.en.md ├── README.md ├── index.js ├── lib ├── access_token.js ├── oauth.js ├── strategy.js └── utils.js ├── package.json └── test ├── access_token.test.js ├── oauth.test.js ├── package.test.js └── strategy.test.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # This is a sample .codeclimate.yml configured for Engine analysis on Code 2 | # Climate Platform. For an overview of the Code Climate Platform, see here: 3 | # http://docs.codeclimate.com/article/300-the-codeclimate-platform 4 | 5 | # Under the engines key, you can configure which engines will analyze your repo. 6 | # Each key is an engine name. For each value, you need to specify enabled: true 7 | # to enable the engine as well as any other engines-specific configuration. 8 | 9 | # For more details, see here: 10 | # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform 11 | 12 | # For a list of all available engines, see here: 13 | # http://docs.codeclimate.com/article/296-engines-available-engines 14 | engines: 15 | eslint: 16 | enabled: true 17 | duplication: 18 | enabled: true 19 | config: 20 | languages: 21 | - javascript 22 | 23 | 24 | 25 | # Engines can analyze files and report issues on them, but you can separately 26 | # decide which files will receive ratings based on those issues. This is 27 | # specified by path patterns under the ratings key. 28 | 29 | # For more details see here: 30 | # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform 31 | 32 | # Note: If the ratings key is not specified, this will result in a 0.0 GPA on your dashboard. 33 | 34 | ratings: 35 | paths: 36 | - "**.js" 37 | 38 | 39 | # You can globally exclude files from being analyzed by any engine using the 40 | # exclude_paths key. 41 | 42 | exclude_paths: 43 | - node_modules/**/* 44 | - test/**/* 45 | #- vendor/**/* 46 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | extends: 'eslint:recommended' 5 | parserOptions: 6 | sourceType: module 7 | rules: 8 | indent: 9 | - 2 10 | - 2 11 | - SwitchCase: 1 12 | linebreak-style: 13 | - 2 14 | - unix 15 | quotes: 16 | - 2 17 | - single 18 | semi: 19 | - 2 20 | - never 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Mac OS 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | script: npm run coveralls 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Vincent Wen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # passport-wechat-enterprise 2 | [Passport](http://passportjs.org/) strategy for authenticating with [Wechat Enterprise Accounts](https://qy.weixin.qq.com/) 3 | using the OAuth 2.0 API. 4 | 5 | Wechat Development Documents: [Enterprise Accounts](http://qydev.weixin.qq.com/wiki/index.php) 6 | 7 | This module lets you authenticate using Wechat in your Node.js applications. 8 | By plugging into Passport, Wechat authentication can be easily and 9 | unobtrusively integrated into any application or framework that supports 10 | [Connect](http://www.senchalabs.org/connect/)-style middleware, including 11 | [Express](http://expressjs.com/),[Loopback](http://loopback.io/). It also supports [Loopback-Component-Passport](https://github.com/strongloop/loopback-component-passport). 12 | 13 | Wechat Official Accounts version, see [passport-wechat-public](https://github.com/wenwei1202/passport-wechat-public) 14 | 15 | 16 | ## Install 17 | 18 | $ npm install passport-wechat-enterprise 19 | 20 | ## Usage 21 | 22 | #### Configure Strategy 23 | 24 | - The Wechat authentication strategy authenticates users using a Wechat 25 | account and OAuth 2.0 tokens. The strategy requires a `verify` callback, which 26 | accepts these credentials and calls `done` providing a user, `options` specifying an corp ID, corp secret, callback URL, and optionally state, scope. The last two are getAccessToken and saveAccessToken functions for access token, and both required. 27 | 28 | `getAccessToken` and `saveAccessToken` are two functions for access token, since wechat has limitation for retrieving access token.For every authentication, it will try to get the access token via `getAccessToken` function,if can't get one it will hit the wechat api `/gettoken` to get a new one then save it via `saveAccessToken` function. 29 | 30 | ``` 31 | passport.use("wechat",new WechatPublicStrategy({ 32 | corpId: CORP_ID, 33 | corpSecret: CORP_SECRET, 34 | callbackURL: "http://localhost:3000/auth/wechat/callback", 35 | state: "state", 36 | scope: "snsapi_base" 37 | }, 38 | function(profile, done) { 39 | User.findOrCreate({ userId: profile.UserId }, function (err, user) { 40 | return done(err, user); 41 | }); 42 | }, 43 | function getAccessToken(cb) { ... }, 44 | function saveAccessToken(accessToken,cb){ ... } 45 | )); 46 | ``` 47 | 48 | #### Authenticate Requests 49 | 50 | Use `passport.authenticate()`, specifying the strategy with the name `'wechat' or default name 'wechat-enterprise'`, to 51 | authenticate requests. 52 | 53 | 用`passport.authenticate()`在对应的route下,注意strategy名字和passport.use()时一致。 54 | 55 | For example 56 | 57 | ``` 58 | app.get('/auth/wechat', 59 | passport.authenticate('wechat')); 60 | 61 | app.get('/auth/wechat/callback', 62 | passport.authenticate('wechat', { failureRedirect: '/login' }), 63 | function(req, res) { 64 | // Successful authentication, redirect home. 65 | res.redirect('/'); 66 | }); 67 | ``` 68 | 69 | 70 | #### Loopback-Component-Passport 71 | Simplely add the a wechat provider into your `providers.json` file. **Notice:profile.id will be same with UserId.** 72 | 73 | 74 | Please see Strongloop [official documents](https://docs.strongloop.com/pages/releaseview.action?pageId=3836277) for more info about [Loopback-Component-Passport](https://github.com/strongloop/loopback-component-passport). 75 | 76 | ``` 77 | { 78 | "wechat": { 79 | "provider": "wechat", 80 | "module": "passport-wechat-enterprise", 81 | "callbackURL": "/auth/wechat/callback", 82 | "successRedirect": "/auth/wechat/account", 83 | "failureRedirect": "/auth/wechat/failure", 84 | "scope": "snsapi_base", 85 | "corpId": "wxabe757c89bb6d74b", 86 | "corpSecret": "9a62bc24a31d5c7c2b1d053515d276f8", 87 | "authScheme": "OAuth 2.0"/*required*/ 88 | } 89 | } 90 | ``` 91 | 92 | - Since in loopback-component-passport, you won't initialize the Strategy by your own, do the trick, put the `getAccessToken ` and `saveAccessToken ` into the options, it's also acceptable. 93 | 94 | ``` 95 | function getAccessToken(cb) {...}; 96 | function saveAccessToken(accessToken, cb){...}; 97 | for (var s in config) { 98 | var c = config[s]; 99 | c.session = c.session !== false; 100 | if(s === 'wechat') { 101 | c.getAccessToken = getAccessToken; 102 | c.saveAccessToken = saveAccessToken; 103 | } 104 | passportConfigurator.configureProvider(s, c); 105 | } 106 | ``` 107 | ## Additional 108 | - Wechat enterprise authentication only get simple profile,like below, so if you want to the complete profile, still need to hit the users API for more info. 109 | 110 | followers: 111 | 112 | ``` 113 | { 114 | "UserId":"USERID", 115 | "DeviceId":"DEVICEID" 116 | } 117 | ``` 118 | 119 | unfolloers: 120 | 121 | ``` 122 | { 123 | "OpenId":"OPENID", 124 | "DeviceId":"DEVICEID" 125 | } 126 | 127 | ``` 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/passport-wechat-enterprise.svg)](https://badge.fury.io/js/passport-wechat-enterprise) 2 | [![Dependency Status](https://david-dm.org/wenwei1202/passport-wechat-enterprise.svg)](https://david-dm.org/wenwei1202/passport-wechat-enterprise) 3 | [![Build Status](https://travis-ci.org/wenwei1202/passport-wechat-enterprise.svg?branch=master)](https://travis-ci.org/wenwei1202/passport-wechat-enterprise) 4 | [![Coverage Status](https://coveralls.io/repos/github/wenwei1202/passport-wechat-enterprise/badge.svg?branch=master)](https://coveralls.io/github/wenwei1202/passport-wechat-enterprise?branch=master) 5 | [![Code Climate](https://codeclimate.com/github/wenwei1202/passport-wechat-enterprise/badges/gpa.svg)](https://codeclimate.com/github/wenwei1202/passport-wechat-enterprise) 6 | # passport-wechat-enterprise 7 | [Passport](http://passportjs.org/) strategy for authenticating with [Wechat Enterprise Accounts](https://qy.weixin.qq.com/) 8 | using the OAuth 2.0 API. 9 | 10 | Passport的微信企业号OAuth2.0用户验证模块, 支持Express,Strongloop|Loopback. 11 | 12 | [微信企业号开发文档](http://qydev.weixin.qq.com/wiki/index.php) 13 | 14 | 微信公众号,转至 [passport-wechat-public](https://github.com/wenwei1202/passport-wechat-public) 15 | 16 | ## Install 17 | 18 | $ npm install passport-wechat-enterprise 19 | 20 | ## Usage 21 | 22 | #### Configure Strategy 23 | 24 | - 在Passport注册WechatEnterpriseStrategy, Passport.use()的第一个参数是name,可以忽略使用默认的名字’wechat-enterprise'。WechatEnterpriseStrategy的构造函数的参数是options,verify 以及getAccessToken和saveAccessToken。 25 | 26 | options的corpId,corpSecret和callbackURL是必需的,其他为可选。verify函数是验证或创建用户传给done函数, getAccessToken和saveAccessToken用已获得AccessToken和保存新的AccessToken,当`getAccessToken`返回的AccessToken无效时,会通过调用微信的`/gettoken`接口获取新的AccessToken,并用`saveAccessToken`进行保存。`getAccessToken` and `saveAccessToken` 都是必需的。 27 | 28 | 29 | ``` 30 | passport.use("wechat",new WechatPublicStrategy({ 31 | corpId: CORP_ID, 32 | corpSecret: CORP_SECRET, 33 | callbackURL: "http://localhost:3000/auth/wechat/callback", 34 | state: "state", 35 | scope: "snsapi_base" 36 | }, 37 | function(profile, done) { 38 | User.findOrCreate({ userId: profile.UserId }, function (err, user) { 39 | return done(err, user); 40 | }); 41 | }, 42 | function getAccessToken(cb) { ... }, 43 | function saveAccessToken(accessToken,cb){ ... } 44 | )) 45 | ``` 46 | 47 | 48 | #### Authenticate Requests 49 | 50 | 51 | 用`passport.authenticate()`在对应的route下,注意strategy名字和passport.use()时一致。 52 | 53 | For example 54 | 55 | ``` 56 | app.get('/auth/wechat', 57 | passport.authenticate('wechat')); 58 | 59 | app.get('/auth/wechat/callback', 60 | passport.authenticate('wechat', { failureRedirect: '/login' }), 61 | function(req, res) { 62 | // Successful authentication, redirect home. 63 | res.redirect('/'); 64 | }); 65 | ``` 66 | 67 | 68 | #### Loopback-Component-Passport 69 | 70 | [Loopback-Component-Passport 官方文档](https://github.com/strongloop/loopback-component-passport). 71 | 72 | 在`providers.json`加入wechat provider即可,profile的id就是UserId 73 | 74 | ``` 75 | { 76 | "wechat": { 77 | "provider": "wechat", 78 | "module": "passport-wechat-enterprise", 79 | "callbackURL": "/auth/wechat/callback", 80 | "successRedirect": "/auth/wechat/account", 81 | "failureRedirect": "/auth/wechat/failure", 82 | "scope": "snsapi_base", 83 | "corpId": "wxabe757c89bb6d74b", 84 | "corpSecret": "9a62bc24a31d5c7c2b1d053515d276f8", 85 | "authScheme": "OAuth 2.0"/*required*/ 86 | } 87 | } 88 | ``` 89 | 90 | - 在loopback-component-passport中,strategy的初始化之会传入options和verify,所以这里需要把`getAccessToken ` 和 `saveAccessToken ` 放到options里。 91 | 92 | ``` 93 | function getAccessToken(cb) {...}; 94 | function saveAccessToken(accessToken, cb){...}; 95 | for (var s in config) { 96 | var c = config[s]; 97 | c.session = c.session !== false; 98 | if(s === 'wechat') { 99 | c.getAccessToken = getAccessToken; 100 | c.saveAccessToken = saveAccessToken; 101 | } 102 | passportConfigurator.configureProvider(s, c); 103 | } 104 | ``` 105 | ## Additional 106 | - 微信的企业用户验证只获得了用户基本信息,实际只获得了id,需要详细的用户信息仍需要调用微信的Users API. 107 | 108 | 已关注: 109 | 110 | ``` 111 | { 112 | "UserId":"USERID", 113 | "DeviceId":"DEVICEID" 114 | } 115 | ``` 116 | 117 | 未关注: 118 | 119 | ``` 120 | { 121 | "OpenId":"OPENID", 122 | "DeviceId":"DEVICEID" 123 | } 124 | 125 | ``` 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Strategy = require('./lib/strategy') 5 | var AccessToken = require('./lib/access_token') 6 | 7 | /** 8 | * Expose `Strategy` directly from package. 9 | */ 10 | exports = module.exports = Strategy 11 | 12 | /** 13 | * Export constructors. 14 | */ 15 | exports.Strategy = Strategy 16 | 17 | /** 18 | * Export AccessToken 19 | */ 20 | exports.AccessToken = AccessToken 21 | -------------------------------------------------------------------------------- /lib/access_token.js: -------------------------------------------------------------------------------- 1 | var AccessToken = function(access_token, expires_in, create_at) { 2 | if (!(this instanceof AccessToken)) { 3 | return new AccessToken(access_token, expires_in, create_at) 4 | } 5 | if (!access_token || !expires_in || !create_at) { 6 | throw new Error('\'access_token\', \'expires_in\' and \'create_at\' properties are required.') 7 | } 8 | this.access_token = access_token 9 | this.expires_in = expires_in 10 | this.create_at = create_at 11 | } 12 | 13 | AccessToken.prototype.isExpired = function() { 14 | return (this.create_at + this.expires_in * 1000) < Date.now() 15 | } 16 | 17 | module.exports = AccessToken 18 | -------------------------------------------------------------------------------- /lib/oauth.js: -------------------------------------------------------------------------------- 1 | const querystring = require('querystring') 2 | const request = require('request') 3 | const AccessToken = require('./access_token.js') 4 | 5 | const AuthorizeUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize' 6 | const AccessTokenUrl = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' 7 | const UserInfoUrl = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo' 8 | 9 | 10 | const OAuth = function(corpId, corpSecret, getAccessToken, saveAccessToken) { 11 | if (!corpId || !corpSecret) { 12 | throw new Error('Wechat Enterprise OAuth requires \'corpId\' and \'corpSecret\'') 13 | } 14 | if (!getAccessToken || !saveAccessToken) { 15 | throw new Error('Wechat Enterprise OAuth requires \'getAccessToken\' and \'saveAccessToken\'') 16 | } 17 | 18 | if (!(this instanceof OAuth)) { 19 | return new OAuth(corpId, corpSecret, getAccessToken, saveAccessToken) 20 | } 21 | this._corpId = corpId 22 | this._corpSecret = corpSecret 23 | this._getAccessToken = getAccessToken 24 | this._saveAccessToken = saveAccessToken 25 | } 26 | 27 | 28 | OAuth.prototype.getAuthorizeUrl = function(options) { 29 | const params = {} 30 | params['appid'] = this._corpId 31 | params['redirect_uri'] = options.redirect_uri 32 | params['response_type'] = 'code' 33 | params['scope'] = options.scope || 'snsapi_base' 34 | params['state'] = options.state || 'state' 35 | return AuthorizeUrl + '?' + querystring.stringify(params) + '#wechat_redirect' 36 | } 37 | 38 | OAuth.prototype.getOAuthAccessToken = function(callback) { 39 | const params = {} 40 | params['corpid'] = this._corpId 41 | params['corpsecret'] = this._corpSecret 42 | const url = AccessTokenUrl + '?' + querystring.stringify(params) 43 | const self = this 44 | wechatRequest(url, function(err, result) { 45 | if (err) { 46 | return callback(err) 47 | } 48 | const accessToken = new AccessToken(result.access_token, result.expires_in, Date.now()) 49 | self._saveAccessToken(accessToken) 50 | callback(null, accessToken) 51 | }) 52 | } 53 | 54 | 55 | OAuth.prototype.getAccessToken = function(callback) { 56 | const self = this 57 | this._getAccessToken(function(err, accessToken) { 58 | if (err || !accessToken || accessToken.isExpired()) { 59 | self.getOAuthAccessToken(callback) 60 | } else { 61 | callback(null, accessToken) 62 | } 63 | }) 64 | 65 | } 66 | 67 | OAuth.prototype.getUserInfo = function(accessToken, code, callback) { 68 | const params = {} 69 | params['access_token'] = accessToken.access_token 70 | params['code'] = code 71 | const url = UserInfoUrl + '?' + querystring.stringify(params) 72 | wechatRequest(url, callback) 73 | } 74 | 75 | function wechatRequest(url, callback) { 76 | request(url, function(err, res, body) { 77 | if (err) return callback(err) 78 | var result = null 79 | try { 80 | result = JSON.parse(body) 81 | } catch (e) { 82 | return callback(e) 83 | } 84 | if (result.errcode) return callback(result) 85 | callback(null, result) 86 | }) 87 | } 88 | 89 | module.exports = OAuth 90 | -------------------------------------------------------------------------------- /lib/strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var passport = require('passport-strategy') 5 | var url = require('url') 6 | var util = require('util') 7 | var utils = require('./utils') 8 | var OAuth2 = require('./oauth') 9 | var async = require('async') 10 | 11 | function WechatEnterpriseStrategy(options, verify, getAccessToken, saveAccessToken) { 12 | options = options || {} 13 | 14 | if (!verify) { 15 | throw new Error('WechatEnterpriseStrategy requires a verify callback') 16 | } 17 | 18 | if (!options.corpId) { 19 | throw new Error('WechatEnterpriseStrategy requires a corpId option') 20 | } 21 | if (!options.corpSecret) { 22 | throw new Error('WechatEnterpriseStrategy requires a corpSecret option') 23 | } 24 | 25 | var _getAccessToken = getAccessToken || options.getAccessToken 26 | var _saveAccessToken = saveAccessToken || options.saveAccessToken 27 | 28 | if (!_getAccessToken || !_saveAccessToken) { 29 | throw new Error('WechatEnterpriseStrategy requires \'getAccessToken\' and \'saveAccessToken\'') 30 | } 31 | 32 | passport.Strategy.call(this) 33 | this.name = 'wechat-enterprise' 34 | this._verify = verify 35 | this._oauth = new OAuth2(options.corpId, options.corpSecret, _getAccessToken, _saveAccessToken) 36 | this._callbackURL = options.callbackURL 37 | this._scope = options.scope 38 | this._scopeSeparator = options.scopeSeparator || ' ' 39 | this._state = options.state 40 | this._passReqToCallback = options.passReqToCallback 41 | } 42 | 43 | /** 44 | * Inherit from `passport.Strategy`. 45 | */ 46 | util.inherits(WechatEnterpriseStrategy, passport.Strategy) 47 | 48 | 49 | /** 50 | * Authenticate request by delegating to a service provider using OAuth 2.0. 51 | * 52 | * @param {Object} req 53 | * @api protected 54 | */ 55 | WechatEnterpriseStrategy.prototype.authenticate = function(req, options) { 56 | options = options || {} 57 | 58 | var self = this 59 | var callbackURL = options.callbackURL || this._callbackURL 60 | if (callbackURL) { 61 | var parsed = url.parse(callbackURL) 62 | if (!parsed.protocol) { 63 | // The callback URL is relative, resolve a fully qualified URL from the 64 | // URL of the originating request. 65 | callbackURL = url.resolve(utils.originalURL(req, { 66 | proxy: this._trustProxy 67 | }), callbackURL) 68 | } 69 | } 70 | var params = {} 71 | 72 | if (req.query && req.query.code) { 73 | var code = req.query.code 74 | async.waterfall([ 75 | function(cb) { 76 | self._oauth.getAccessToken(cb) 77 | }, 78 | function(accessToken, cb) { 79 | self._oauth.getUserInfo(accessToken, code, cb) 80 | } 81 | ], function(err, profile) { 82 | if (err) { 83 | return self.error(err) 84 | } 85 | profile.id = profile.UserId 86 | if (profile.UserId) { 87 | verifyResult(profile, verified) 88 | } else { 89 | self.fail() 90 | } 91 | }) 92 | } else { 93 | params.redirect_uri = callbackURL 94 | var scope = options.scope || this._scope 95 | if (scope) { 96 | params.scope = scope 97 | } 98 | params.state = options.state || this._state 99 | var location = this._oauth.getAuthorizeUrl(params) 100 | this.redirect(location, 302) 101 | } 102 | 103 | function verified(err, user, info) { 104 | if (err) { 105 | return self.error(err) 106 | } 107 | if (!user) { 108 | return self.fail(info) 109 | } 110 | self.success(user, info) 111 | 112 | } 113 | 114 | function verifyResult(profile, verified) { 115 | try { 116 | if (self._passReqToCallback) { 117 | self._verify(req, profile, verified) 118 | } else { 119 | self._verify(profile, verified) 120 | } 121 | } catch (ex) { 122 | return self.error(ex) 123 | } 124 | } 125 | } 126 | 127 | module.exports = WechatEnterpriseStrategy 128 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reconstructs the original URL of the request. 3 | * 4 | * This function builds a URL that corresponds the original URL requested by the 5 | * client, including the protocol (http or https) and host. 6 | * 7 | * If the request passed through any proxies that terminate SSL, the 8 | * `X-Forwarded-Proto` header is used to detect if the request was encrypted to 9 | * the proxy, assuming that the proxy has been flagged as trusted. 10 | * 11 | * @param {http.IncomingMessage} req 12 | * @param {Object} [options] 13 | * @return {String} 14 | * @api private 15 | */ 16 | exports.originalURL = function(req, options) { 17 | options = options || {} 18 | var app = req.app 19 | if (app && app.get && app.get('trust proxy')) { 20 | options.proxy = true 21 | } 22 | var trustProxy = options.proxy 23 | 24 | var proto = (req.headers['x-forwarded-proto'] || '').toLowerCase(), 25 | tls = req.connection.encrypted || (trustProxy && 'https' == proto.split(/\s*,\s*/)[0]), 26 | host = (trustProxy && req.headers['x-forwarded-host']) || req.headers.host, 27 | protocol = tls ? 'https' : 'http', 28 | path = req.url || '' 29 | return protocol + '://' + host + path 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-wechat-enterprise", 3 | "version": "0.2.0", 4 | "description": "Passport Strategy for Wechat Enterprise Account/微信企业号", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "istanbul cover _mocha --report lcovonly -- -R spec", 8 | "coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/wenwei1202/passport-wechat-enterprise" 13 | }, 14 | "keywords": [ 15 | "passport", 16 | "wechat", 17 | "enterprise" 18 | ], 19 | "dependencies": { 20 | "async": "^1.5.2", 21 | "passport-strategy": "1.x.x", 22 | "request": "^2.58.0" 23 | }, 24 | "author": "vincent.wen", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/wenwei1202/passport-wechat-enterprise/issues" 28 | }, 29 | "homepage": "https://github.com/wenwei1202/passport-wechat-enterprise#readme", 30 | "devDependencies": { 31 | "chai": "^3.3.0", 32 | "chai-passport-strategy": "^0.2.0", 33 | "mocha": "^2.3.3", 34 | "istanbul": "^0.4.3", 35 | "proxyquire": "^1.7.7", 36 | "coveralls": "^2.11.9", 37 | "mocha-lcov-reporter": "^1.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/access_token.test.js: -------------------------------------------------------------------------------- 1 | /*global describe it*/ 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | const AccessToken = require('../lib/access_token') 5 | 6 | 7 | describe('AccessToken ', function() { 8 | describe('constructor ', function() { 9 | it('should return the AccessToken as expected', function() { 10 | const token = new AccessToken('token123', 7200, Date.now()) 11 | expect(token).to.have.property('access_token', 'token123') 12 | expect(token).to.have.property('expires_in', 7200, Date.now()) 13 | expect(token).to.have.property('create_at') 14 | }) 15 | 16 | it('should return the AccessToken when "new" is missed', function() { 17 | const token = AccessToken('token123', 7200, Date.now()) 18 | expect(token).to.be.instanceof(AccessToken) 19 | }) 20 | 21 | it('should throw error when any of required arguments is null ', function() { 22 | const args = [ 23 | ['token', 7200, null], 24 | ['token', null, Date.now()], 25 | [null, 7200, Date.now()] 26 | ] 27 | args.forEach((arg) => { 28 | expect(function() { 29 | AccessToken.apply(null, arg) 30 | }).to.throw(Error) 31 | }) 32 | 33 | }) 34 | }) 35 | 36 | describe('isExpired function ', function() { 37 | it('should return true if token is expired', function() { 38 | const token = new AccessToken('token123', 7200, Date.now()) 39 | token.create_at -= 7201 * 1000 40 | expect(token.isExpired()).to.be.true 41 | }) 42 | 43 | it('should return false if token is valid', function() { 44 | const token = new AccessToken('token123', 7200, Date.now()) 45 | expect(token.isExpired()).to.be.false 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/oauth.test.js: -------------------------------------------------------------------------------- 1 | /*global describe it*/ 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | const proxyquire = require('proxyquire') 5 | const requstStub = function(url, callback) { 6 | const regex = /.+corpid=(.+)&.+$/ 7 | const match = regex.exec(url) 8 | if (match && match[1] === 'error') { 9 | callback(new Error('error')) 10 | } else if (match && match[1] === 'invalid') { 11 | callback(null, {}, '{"format": error') 12 | } else if (match && match[1] === 'wechat_error') { 13 | const err = { 14 | errcode: 43003, 15 | errmsg: 'require https' 16 | } 17 | callback(null, {}, JSON.stringify(err)) 18 | } else { 19 | if (url.startsWith(AccessTokenUrl)) { 20 | const token = { 21 | access_token: 'token001', 22 | expires_in: 7200 23 | } 24 | callback(null, {}, JSON.stringify(token)) 25 | } else if (url.startsWith(UserInfoUrl)) { 26 | const userInfo = { 27 | 'UserId': 'USERID', 28 | 'DeviceId': 'DEVICEID' 29 | } 30 | callback(null, {}, JSON.stringify(userInfo)) 31 | } 32 | } 33 | } 34 | const OAuth = proxyquire('../lib/oauth', { 35 | 'request': requstStub 36 | }) 37 | 38 | const AccessToken = require('../lib/access_token') 39 | 40 | const AccessTokenUrl = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' 41 | const UserInfoUrl = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo' 42 | 43 | describe('OAuth ', () => { 44 | describe('Constructor ', () => { 45 | it('should throw errors when any of are not set', () => { 46 | expect(() => { 47 | new OAuth(null, 'corpSecret', () => {}, () => {}) 48 | }).to.throw(/Wechat Enterprise OAuth requires \'corpId\' and \'corpSecret\'/) 49 | expect(() => { 50 | new OAuth('corpId', '', () => {}, () => {}) 51 | }).to.throw(/Wechat Enterprise OAuth requires \'corpId\' and \'corpSecret\'/) 52 | expect(() => { 53 | new OAuth('corpId', 'corpSecret', null, () => {}) 54 | }).to.throw(/Wechat Enterprise OAuth requires \'getAccessToken\' and \'saveAccessToken\'/) 55 | expect(() => { 56 | new OAuth('corpId', 'corpSecret', () => {}, null) 57 | }).to.throw(/Wechat Enterprise OAuth requires \'getAccessToken\' and \'saveAccessToken\'/) 58 | }) 59 | 60 | it('should return the OAuth when "new" is missed', () => { 61 | const oauth = OAuth('corpId', 'corpSecret', () => {}, () => {}) 62 | expect(oauth).to.be.instanceof(OAuth) 63 | }) 64 | }) 65 | 66 | 67 | describe('getAuthorizeUrl ', () => { 68 | const oauth = OAuth('corpId', 'corpSecret', () => {}, () => {}) 69 | it('should the authorize url ', () => { 70 | const options = { 71 | redirect_uri: '/auth/wechat/callback', 72 | scope: 'snsapi_base', 73 | state: 'st' 74 | } 75 | const authUrl = oauth.getAuthorizeUrl(options) 76 | expect(authUrl).to.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=corpId&redirect_uri=%2Fauth%2Fwechat%2Fcallback&response_type=code&scope=snsapi_base&state=st#wechat_redirect') 77 | }) 78 | }) 79 | 80 | describe('getOAuthAccessToken ', () => { 81 | const oauth = OAuth('corpId', 'corpSecret', () => {}, () => {}) 82 | it('should get the access token ', (done) => { 83 | oauth.getOAuthAccessToken((err, accessToken) => { 84 | expect(err).to.be.null 85 | expect(accessToken).to.have.property('access_token') 86 | expect(accessToken).to.have.property('expires_in') 87 | done() 88 | }) 89 | }) 90 | it('should callback with error if error occurs when requesting ', (done) => { 91 | oauth._corpId = 'error' 92 | oauth.getOAuthAccessToken((err) => { 93 | expect(err).to.be.an('error') 94 | done() 95 | }) 96 | }) 97 | 98 | it('should callback with error if error occurs when requesting ', (done) => { 99 | oauth._corpId = 'invalid' 100 | oauth.getOAuthAccessToken((err) => { 101 | expect(err).to.be.an('error') 102 | done() 103 | }) 104 | }) 105 | 106 | it('should callback with error if wechat return an error ', (done) => { 107 | oauth._corpId = 'wechat_error' 108 | oauth.getOAuthAccessToken((err) => { 109 | expect(err).to.deep.equal({ 110 | errcode: 43003, 111 | errmsg: 'require https' 112 | }) 113 | done() 114 | }) 115 | }) 116 | }) 117 | 118 | describe('getAccessToken ', () => { 119 | const getAccessToken = function(cb) { 120 | cb(null, new AccessToken('token', 7200, Date.now())) 121 | } 122 | const oauth = OAuth('corpId', 'corpSecret', getAccessToken, () => {}) 123 | oauth.getOAuthAccessToken = function(cb) { 124 | cb(null, { 125 | 'access_token': 'new_token', 126 | 'expires_in': 7200 127 | }) 128 | } 129 | it('should callback with existing access token if available ', (done) => { 130 | oauth.getAccessToken((err, accessToken) => { 131 | expect(accessToken).to.have.property('access_token', 'token') 132 | done() 133 | }) 134 | }) 135 | 136 | it('should request new access token from wechat endpoint if no access token is availble ', (done) => { 137 | oauth._getAccessToken = function(cb) { 138 | cb() 139 | } 140 | oauth.getAccessToken((err, accessToken) => { 141 | expect(accessToken).to.have.property('access_token', 'new_token') 142 | done() 143 | }) 144 | }) 145 | }) 146 | 147 | describe('getUserInfo ', () => { 148 | const oauth = OAuth('corpId', 'corpSecret', () => {}, () => {}) 149 | const code = 'code' 150 | const accessToken = new AccessToken('token', 7200, Date.now()) 151 | it('should the authorize url ', () => { 152 | oauth.getUserInfo(accessToken, code, (err, profile) => { 153 | expect(profile).to.deep.equal({ 154 | 'UserId': 'USERID', 155 | 'DeviceId': 'DEVICEID' 156 | }) 157 | }) 158 | }) 159 | }) 160 | 161 | 162 | }) 163 | -------------------------------------------------------------------------------- /test/package.test.js: -------------------------------------------------------------------------------- 1 | /*global describe,it*/ 2 | var expect = require('chai').expect 3 | var strategy = require('..') 4 | 5 | describe('passport-wechat-enterprise', function() { 6 | 7 | it('should export Strategy constructor directly from package', function() { 8 | expect(strategy).to.be.a('function') 9 | expect(strategy).to.equal(strategy.Strategy) 10 | }) 11 | 12 | it('should export Strategy constructor', function() { 13 | expect(strategy.Strategy).to.be.a('function') 14 | }) 15 | 16 | }) 17 | -------------------------------------------------------------------------------- /test/strategy.test.js: -------------------------------------------------------------------------------- 1 | /*global describe it before */ 2 | const chai = require('chai') 3 | chai.use(require('chai-passport-strategy')) 4 | const expect = chai.expect 5 | const proxyquire = require('proxyquire') 6 | const utilsStub = { 7 | originalURL: function() { 8 | return 'http://localhost' 9 | } 10 | } 11 | const WechatStrategy = proxyquire('../lib/strategy', { 12 | './utils': utilsStub 13 | }) 14 | const AccessToken = require('../lib/access_token') 15 | 16 | 17 | describe('Strategy', function() { 18 | 19 | describe('constructor', function() { 20 | 21 | const strategy = generateDefaultStrategy() 22 | it('should be named wechat-enterprise', function() { 23 | expect(strategy.name).to.equal('wechat-enterprise') 24 | }) 25 | }) 26 | 27 | 28 | describe('constructed with undefined options', function() { 29 | const options = { 30 | corpId: 'corpid', 31 | corpSecret: 'corpsecret', 32 | getAccessToken: getAccessToken, 33 | saveAccessToken: saveAccessToken 34 | } 35 | var opts 36 | 37 | it('should throw an error when verify function is undefined', function() { 38 | expect(() => { 39 | new WechatStrategy(options) 40 | }).to.throw('WechatEnterpriseStrategy requires a verify callback') 41 | }) 42 | 43 | it('should throw an error when corpId is undefined', function() { 44 | opts = Object.assign({}, options) 45 | opts.corpId = undefined 46 | expect(() => { 47 | new WechatStrategy(opts, () => {}) 48 | }).to.throw('WechatEnterpriseStrategy requires a corpId option') 49 | }) 50 | 51 | it('should throw an error when corpSecret is undefined', function() { 52 | opts = Object.assign({}, options) 53 | opts.corpSecret = undefined 54 | expect(() => { 55 | new WechatStrategy(opts, () => {}) 56 | }).to.throw('WechatEnterpriseStrategy requires a corpSecret option') 57 | }) 58 | 59 | it('should throw an error when corpSecret is undefined', function() { 60 | opts = Object.assign({}, options) 61 | opts.corpSecret = undefined 62 | expect(() => { 63 | new WechatStrategy(opts, () => {}) 64 | }).to.throw('WechatEnterpriseStrategy requires a corpSecret option') 65 | }) 66 | 67 | it('should throw an error when getAccessToken function is undefined', function() { 68 | opts = Object.assign({}, options) 69 | opts.getAccessToken = undefined 70 | expect(() => { 71 | new WechatStrategy(opts, () => {}) 72 | }).to.throw('WechatEnterpriseStrategy requires \'getAccessToken\' and \'saveAccessToken\'') 73 | }) 74 | 75 | it('should throw an error when saveAccessToken function is undefined', function() { 76 | opts = Object.assign({}, options) 77 | opts.saveAccessToken = undefined 78 | expect(() => { 79 | new WechatStrategy(opts, () => {}) 80 | }).to.throw('WechatEnterpriseStrategy requires \'getAccessToken\' and \'saveAccessToken\'') 81 | }) 82 | }) 83 | 84 | describe('authorization request with authorization parameters', function() { 85 | const strategy = generateDefaultStrategy() 86 | 87 | var url 88 | 89 | before(function(done) { 90 | chai.passport.use(strategy) 91 | .redirect(function(u) { 92 | url = u 93 | done() 94 | }) 95 | .authenticate() 96 | }) 97 | 98 | it('should be redirected', function() { 99 | expect(url).to.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=ABC123&redirect_uri=http%3A%2F%2Flocalhost%2Fauth%2Fwechat%2Fcallback&response_type=code&scope=snsapi_base&state=state#wechat_redirect') 100 | }) 101 | }) 102 | 103 | 104 | describe('error caused by invalid code sent to token endpoint', function() { 105 | const strategy = generateDefaultStrategy() 106 | 107 | // inject a "mock" oauth2 instance 108 | strategy._oauth.getAccessToken = function(callback) { 109 | return callback({ 110 | 'errcode': 40029, 111 | 'errmsg': 'invalid code' 112 | }) 113 | } 114 | 115 | var err 116 | 117 | before(function(done) { 118 | chai.passport.use(strategy) 119 | .error(function(e) { 120 | err = e 121 | done() 122 | }) 123 | .req(function(req) { 124 | req.query = {} 125 | req.query.code = 'SplxlOBeZQQYbYS6WxSbIA+ALT1' 126 | }) 127 | .authenticate() 128 | }) 129 | 130 | it('should error', function() { 131 | expect(err.errcode).to.equal(40029) 132 | expect(err.errmsg).to.equal('invalid code') 133 | }) 134 | }) 135 | 136 | describe('fetch user info', () => { 137 | const options = { 138 | corpId: 'ABC123', 139 | corpSecret: 'secret' 140 | } 141 | const getUserInfo = (accessToken, code, callback) => { 142 | var userInfo = null 143 | var error = null 144 | switch (code) { 145 | case 'subscribed': 146 | { 147 | userInfo = { 148 | 'UserId': 'USERID', 149 | 'DeviceId': 'DEVICEID' 150 | } 151 | break 152 | } 153 | case 'unsubscribed': 154 | { 155 | userInfo = { 156 | 'UserId': 'OPENID', 157 | 'DeviceId': 'DEVICEID' 158 | } 159 | break 160 | } 161 | case 'no_user': 162 | { 163 | userInfo = {} 164 | break 165 | } 166 | default: 167 | { 168 | error = { 169 | 'errcode': 40029, 170 | 'errmsg': 'invalid code' 171 | } 172 | } 173 | } 174 | callback(error, userInfo) 175 | } 176 | 177 | it('should fetch info including userId if user has subscribed', function(done) { 178 | const strategy = new WechatStrategy(options, (profile, verified) => { 179 | expect(profile.id).to.equal('USERID') 180 | done() 181 | }, getAccessToken, () => {}) 182 | strategy._oauth.getUserInfo = getUserInfo 183 | 184 | chai.passport.use(strategy) 185 | .req(function(req) { 186 | req.query = {} 187 | req.query.code = 'subscribed' 188 | }) 189 | .authenticate() 190 | }) 191 | 192 | it('should fetch info including openId if user has not subscribed', function(done) { 193 | const strategy = new WechatStrategy(options, (profile, verified) => { 194 | expect(profile.UserId).to.equal('OPENID') 195 | done() 196 | }, getAccessToken, () => {}) 197 | strategy._oauth.getUserInfo = getUserInfo 198 | 199 | chai.passport.use(strategy) 200 | .req(function(req) { 201 | req.query = {} 202 | req.query.code = 'unsubscribed' 203 | }) 204 | .authenticate() 205 | }) 206 | 207 | it('should return error the code is invalid', function(done) { 208 | var err 209 | const strategy = new WechatStrategy(options, (profile, verified) => { 210 | expect(err.errcode).to.equal(40029) 211 | expect(err.errmsg).to.equal('invalid code') 212 | done() 213 | }, getAccessToken, () => {}) 214 | strategy._oauth.getUserInfo = getUserInfo 215 | 216 | chai.passport.use(strategy) 217 | .error(function(e) { 218 | err = e 219 | done() 220 | }) 221 | .req(function(req) { 222 | req.query = {} 223 | req.query.code = 'invalid_code' 224 | }) 225 | .authenticate() 226 | }) 227 | 228 | it('should fail if UserId is null', function(done) { 229 | const strategy = new WechatStrategy(options, (profile, verified) => {}, getAccessToken, () => {}) 230 | strategy._oauth.getUserInfo = getUserInfo 231 | 232 | chai.passport.use(strategy) 233 | .req(function(req) { 234 | req.query = {} 235 | req.query.code = 'no_user' 236 | }) 237 | .fail(function() { 238 | done() 239 | }) 240 | .authenticate() 241 | }) 242 | 243 | }) 244 | 245 | describe('verify function ', () => { 246 | const getUserInfo = (accessToken, code, callback) => { 247 | const userInfo = { 248 | 'UserId': 'OPENID', 249 | 'DeviceId': 'DEVICEID' 250 | } 251 | callback(null, userInfo) 252 | } 253 | 254 | const options = { 255 | corpId: 'ABC123', 256 | corpSecret: 'secret', 257 | passReqToCallback: false 258 | } 259 | 260 | it('should pass 3 arguments when passReqToCallback is true', (done) => { 261 | options.passReqToCallback = true 262 | const strategy = new WechatStrategy(options, function(req, profile, verified) { 263 | expect(arguments.length).to.equal(3) 264 | done() 265 | }, getAccessToken, saveAccessToken) 266 | strategy._oauth.getUserInfo = getUserInfo 267 | chai.passport.use(strategy) 268 | .req(function(req) { 269 | req.query = {} 270 | req.query.code = 'code' 271 | }) 272 | .authenticate() 273 | }) 274 | 275 | it('should pass 2 arguments when when passReqToCallback is false', (done) => { 276 | options.passReqToCallback = false 277 | const strategy = new WechatStrategy(options, function(req, profile, verified) { 278 | expect(arguments.length).to.equal(2) 279 | done() 280 | }, getAccessToken, saveAccessToken) 281 | strategy._oauth.getUserInfo = getUserInfo 282 | chai.passport.use(strategy) 283 | .req(function(req) { 284 | req.query = {} 285 | req.query.code = 'code' 286 | }) 287 | .authenticate() 288 | }) 289 | it('should get an error when verify function throw any error', (done) => { 290 | options.passReqToCallback = false 291 | const strategy = new WechatStrategy(options, function(req, profile, verified) { 292 | throw new Error('Verify Error') 293 | }, getAccessToken, saveAccessToken) 294 | strategy._oauth.getUserInfo = getUserInfo 295 | chai.passport.use(strategy) 296 | .req(function(req) { 297 | req.query = {} 298 | req.query.code = 'code' 299 | }) 300 | .error((err) => { 301 | expect(err).to.be.an('error') 302 | done() 303 | }) 304 | .authenticate() 305 | }) 306 | }) 307 | 308 | describe('verified function ', () => { 309 | const getUserInfo = (accessToken, code, callback) => { 310 | const userInfo = { 311 | 'UserId': 'OPENID', 312 | 'DeviceId': 'DEVICEID' 313 | } 314 | callback(null, userInfo) 315 | } 316 | 317 | const options = { 318 | corpId: 'ABC123', 319 | corpSecret: 'secret', 320 | passReqToCallback: false 321 | } 322 | 323 | it('should get an error when get an error', (done) => { 324 | const strategy = new WechatStrategy(options, function(profile, verified) { 325 | verified(new Error('error')) 326 | }, getAccessToken, saveAccessToken) 327 | strategy._oauth.getUserInfo = getUserInfo 328 | chai.passport.use(strategy) 329 | .req(function(req) { 330 | req.query = {} 331 | req.query.code = 'code' 332 | }) 333 | .error((err) => { 334 | expect(err).to.be.an('error') 335 | done() 336 | }) 337 | .authenticate() 338 | }) 339 | 340 | it('should fail the auth when no user is found', (done) => { 341 | const strategy = new WechatStrategy(options, function(profile, verified) { 342 | verified(null, null, 'no user is found.') 343 | }, getAccessToken, saveAccessToken) 344 | strategy._oauth.getUserInfo = getUserInfo 345 | chai.passport.use(strategy) 346 | .req(function(req) { 347 | req.query = {} 348 | req.query.code = 'code' 349 | }) 350 | .fail((info) => { 351 | expect(info).to.equal('no user is found.') 352 | done() 353 | }) 354 | .authenticate() 355 | }) 356 | 357 | it('should be successful when user is set', (done) => { 358 | const strategy = new WechatStrategy(options, function(profile, verified) { 359 | verified(null, profile) 360 | }, getAccessToken, saveAccessToken) 361 | strategy._oauth.getUserInfo = getUserInfo 362 | chai.passport.use(strategy) 363 | .req(function(req) { 364 | req.query = {} 365 | req.query.code = 'code' 366 | }) 367 | .success((profile) => { 368 | expect(profile).to.be.an('object') 369 | done() 370 | }) 371 | .authenticate() 372 | }) 373 | 374 | }) 375 | 376 | function generateDefaultStrategy() { 377 | const options = { 378 | corpId: 'ABC123', 379 | corpSecret: 'secret', 380 | callbackURL: '/auth/wechat/callback', 381 | scope: 'snsapi_base' 382 | } 383 | const strategy = new WechatStrategy(options, 384 | () => {}, 385 | getAccessToken, 386 | saveAccessToken) 387 | return strategy 388 | } 389 | 390 | function getAccessToken(cb) { 391 | const token = new AccessToken('token123', 7200, Date.now()) 392 | cb(null, token) 393 | } 394 | 395 | function saveAccessToken(accessToken) { 396 | 397 | } 398 | }) 399 | --------------------------------------------------------------------------------