├── .gitignore ├── lib ├── index.js ├── profile.js ├── strategy.js └── oauth2.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Strategy = require('./strategy'); 5 | 6 | 7 | /** 8 | * Expose `Strategy` directly from package. 9 | */ 10 | exports = module.exports = Strategy; 11 | 12 | /** 13 | * Export constructors. 14 | */ 15 | exports.Strategy = Strategy; -------------------------------------------------------------------------------- /lib/profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse profile. 3 | * 4 | * @param {Object|String} json 5 | * @return {Object} 6 | * @api private 7 | */ 8 | exports.parse = function(json) { 9 | if ('string' == typeof json) { 10 | json = JSON.parse(json); 11 | } 12 | 13 | var profile = {}; 14 | profile.id = String(json.unionid); 15 | profile.displayName = json.nickname; 16 | profile.profileUrl = json.headimgurl; 17 | if (json.email) { 18 | profile.emails = [{ 19 | value: json.email 20 | }]; 21 | } 22 | 23 | return profile; 24 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-weixin-plus", 3 | "version": "0.0.4", 4 | "description": "passport oauth2 strategy for weixin, Support different appids and appsecrets based on different requests", 5 | "main": "./lib", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "passport", 11 | "weixin", 12 | "oauth2", 13 | "strategy" 14 | ], 15 | "author": "lutaoact", 16 | "license": "MIT", 17 | "dependencies": { 18 | "passport-oauth2": "^1.1.2" 19 | }, 20 | "devDependencies": {}, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/lutaoact/passport-weixin-plus.git" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## passport-weixin-plus 2 | passport oauth2 strategy for weixin 3 | Support multiple appids and appsecrets based on different requests 4 | 5 | ### Install 6 | 7 | ``` bash 8 | npm install passport-weixin-plus 9 | ``` 10 | 11 | ### Usage 12 | 13 | ``` js 14 | var passport = require('passport') 15 | , WeixinStrategy = require('passport-weixin-plus') 16 | ; 17 | 18 | passport.use(new WeixinStrategy({ 19 | clientID: 'CLIENTID' 20 | , clientSecret: 'CLIENT SECRET' 21 | , callbackURL: 'CALLBACK URL' 22 | , requireState: false 23 | , scope: 'snsapi_login' 24 | }, function(accessToken, refreshToken, profile, done){ 25 | done(null, profile); 26 | })); 27 | 28 | or 29 | 30 | passport.use(new WeixinStrategy({ 31 | clientID: function(req) { 32 | //return different appid for different req 33 | }, 34 | clientSecret: function(req) { 35 | //return different secret for different req 36 | }, 37 | callbackURL: function(req) { 38 | //return different callbackURL for different req 39 | }, 40 | requireState: false, 41 | scope: 'snsapi_login' 42 | }, function(accessToken, refreshToken, profile, done){ 43 | done(null, profile); 44 | })); 45 | ``` 46 | 47 | 48 | ### Example 49 | 50 | ```js 51 | var config = { 52 | weixinAuth: { 53 | host2appKeyMap: { 54 | 'demo.mydomain.cn': { 55 | appkey: '1xxxxxxxxxxxxxxxxx', 56 | secret: '1yyyyyyyyyyyyyyyyyyyyyyyy' 57 | }, 58 | 'xxx.mydomain.cn': { 59 | appkey: '2xxxxxxxxxxxxxxxxx', 60 | secret: '2yyyyyyyyyyyyyyyyyyyyyyyy' 61 | }, 62 | 'www.mydomain.cn': { 63 | appkey: '3xxxxxxxxxxxxxxxxx', 64 | secret: '3yyyyyyyyyyyyyyyyyyyyyyyy' 65 | } 66 | }, 67 | callbackURL: '/auth/weixin/callback' 68 | }, 69 | }; 70 | 71 | passport.use(new WeixinStrategy({ 72 | clientID: function(req) { 73 | return config.weixinAuth.host2appKeyMap[req.headers.host].appkey; 74 | }, 75 | clientSecret: function(req) { 76 | return config.weixinAuth.host2appKeyMap[req.headers.host].secret; 77 | }, 78 | callbackURL: function(req) { 79 | return "http://" + req.headers.host + config.weixinAuth.callbackURL; 80 | }, 81 | requireState: false, 82 | scope: 'snsapi_login', 83 | passReqToCallback: true 84 | }, function(req, token, refreshToken, profile, done) { 85 | done(null, profile); 86 | })); 87 | 88 | router.get('/auth/weixin/callback', function(req, res, next) { 89 | passport.authenticate('weixin', function(err, user, info) { 90 | if (err) return next(err) 91 | 92 | //do anything as you like 93 | 94 | res.send(user); 95 | })(req, res, next); 96 | }); 97 | ``` 98 | 99 | ### License 100 | MIT 101 | -------------------------------------------------------------------------------- /lib/strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var util = require('util') 5 | , OAuth2Strategy = require('passport-oauth2') 6 | , Profile = require('./profile') 7 | , InternalOAuthError = require('passport-oauth2').InternalOAuthError 8 | , localOauth2 = require('./oauth2') 9 | ; 10 | 11 | 12 | /** 13 | * `Strategy` constructor. 14 | * 15 | * The weixin authentication strategy authenticates requests by delegating to 16 | * weixin using the OAuth 2.0 protocol. 17 | * 18 | * Applications must supply a `verify` callback which accepts an `accessToken`, 19 | * `refreshToken` and service-specific `profile`, and then calls the `done` 20 | * callback supplying a `user`, which should be set to `false` if the 21 | * credentials are not valid. If an exception occured, `err` should be set. 22 | * 23 | * Options: 24 | * - `clientID` your weixin application's Client ID 25 | * - `clientSecret` your weixin application's Client Secret 26 | * - `callbackURL` URL to which weixin will redirect the user after granting authorization 27 | * - `scope` valid scopes include: 28 | * 'snsapi_base', 'snsapi_login'. 29 | * (see http://developer.github.com/v3/oauth/#scopes for more info) 30 | * — `userAgent` optional, you can set your own userAgent 31 | * 32 | * Examples: 33 | * 34 | * passport.use(new WeixinStrategy({ 35 | * clientID: '123-456-789', 36 | * clientSecret: 'shhh-its-a-secret' 37 | * callbackURL: 'https://www.example.net/auth/weixin/callback', 38 | * userAgent: 'myapp.com' 39 | * }, 40 | * function(accessToken, refreshToken, profile, done) { 41 | * User.findOrCreate(..., function (err, user) { 42 | * done(err, user); 43 | * }); 44 | * } 45 | * )); 46 | * 47 | * @param {Object} options 48 | * @param {Function} verify 49 | * @api public 50 | */ 51 | function Strategy(options, verify) { 52 | options = options || {}; 53 | options.authorizationURL = options.authorizationURL || 'https://open.weixin.qq.com/connect/qrconnect'; 54 | options.tokenURL = options.tokenURL || 'https://api.weixin.qq.com/sns/oauth2/access_token'; 55 | options.scopeSeparator = options.scopeSeparator || ','; 56 | options.customHeaders = options.customHeaders || {}; 57 | options.scope = options.scope || 'snsapi_login'; 58 | 59 | if (!options.customHeaders['User-Agent']) { 60 | options.customHeaders['User-Agent'] = options.userAgent || 'passport-weixin'; 61 | } 62 | 63 | OAuth2Strategy.call(this, options, verify); 64 | this.name = 'weixin'; 65 | this._appid = options.clientID; 66 | this._secret = options.clientSecret; 67 | this._requireState = options.requireState === undefined ? true : options.requireState; 68 | this._userProfileURL = options.userProfileURL || 'https://api.weixin.qq.com/sns/userinfo'; 69 | 70 | // hack for weixin 71 | this.authenticate = localOauth2.authenticate; 72 | this._loadUserProfile = localOauth2._loadUserProfile; 73 | } 74 | 75 | /** 76 | * Inherit from `OAuth2Strategy`. 77 | */ 78 | util.inherits(Strategy, OAuth2Strategy); 79 | 80 | Strategy.prototype.authorizationParams = function(req, options){ 81 | options.appid = typeof this._appid === 'function' ? this._appid(req) : this._appid; 82 | if(this._requireState && !options.state){ 83 | throw new Error('Authentication Parameter `state` Required'); 84 | }else{ 85 | return options; 86 | } 87 | }; 88 | 89 | Strategy.prototype.tokenParams = function(req, options){ 90 | options.appid = typeof this._appid === 'function' ? this._appid(req) : this._appid; 91 | options.secret = typeof this._secret === 'function' ? this._secret(req) : this._secret; 92 | return options; 93 | }; 94 | 95 | /** 96 | * Retrieve user profile from weixin. 97 | * 98 | * This function constructs a normalized profile, with the following properties: 99 | * 100 | * - `provider` always set to `weixin` 101 | * - `id` the user's unionid ID 102 | * - `displayName` the user's nickname 103 | * - `profileUrl` the URL of the profile for the user on weixin 104 | * 105 | * @param {String} accessToken 106 | * @param {Function} done 107 | * @api protected 108 | */ 109 | Strategy.prototype.userProfile = function(accessToken, openid, done) { 110 | var userProfileURL = this._userProfileURL + '?openid=' + openid; 111 | this._oauth2.get(userProfileURL, accessToken, function(err, body, res) { 112 | var json; 113 | if (err) { 114 | return done(new InternalOAuthError('Failed to fetch user profile', err)); 115 | } 116 | 117 | try { 118 | json = JSON.parse(body); 119 | } catch (ex) { 120 | return done(new Error('Failed to parse user profile')); 121 | } 122 | 123 | var profile = Profile.parse(json); 124 | profile.provider = 'weixin'; 125 | profile._raw = body; 126 | profile._json = json; 127 | 128 | done(null, profile); 129 | }); 130 | } 131 | 132 | 133 | /** 134 | * Expose `Strategy`. 135 | */ 136 | module.exports = Strategy; 137 | -------------------------------------------------------------------------------- /lib/oauth2.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | , util = require('util') 3 | , AuthorizationError = require('passport-oauth2/lib/errors/authorizationerror') 4 | ; 5 | 6 | exports.authenticate = function(req, options) { 7 | options = options || {}; 8 | var self = this; 9 | 10 | if (req.query && req.query.error) { 11 | if (req.query.error == 'access_denied') { 12 | return this.fail({ 13 | message: req.query.error_description 14 | }); 15 | } else { 16 | return this.error(new AuthorizationError(req.query.error_description, req.query.error, req.query.error_uri)); 17 | } 18 | } 19 | 20 | var callbackURL = options.callbackURL || this._callbackURL; 21 | if (typeof callbackURL === 'function') { 22 | callbackURL = callbackURL(req); 23 | } 24 | if (callbackURL) { 25 | var parsed = url.parse(callbackURL); 26 | if (!parsed.protocol) { 27 | // The callback URL is relative, resolve a fully qualified URL from the 28 | // URL of the originating request. 29 | callbackURL = url.resolve(utils.originalURL(req, { 30 | proxy: this._trustProxy 31 | }), callbackURL); 32 | } 33 | } 34 | 35 | if (req.query && req.query.code) { 36 | var code = req.query.code; 37 | 38 | if (this._state) { 39 | if (!req.session) { 40 | return this.error(new Error('OAuth2Strategy requires session support when using state. Did you forget app.use(express.session(...))?')); 41 | } 42 | 43 | var key = this._key; 44 | if (!req.session[key]) { 45 | return this.fail({ 46 | message: 'Unable to verify authorization request state.' 47 | }, 403); 48 | } 49 | var state = req.session[key].state; 50 | if (!state) { 51 | return this.fail({ 52 | message: 'Unable to verify authorization request state.' 53 | }, 403); 54 | } 55 | 56 | delete req.session[key].state; 57 | if (Object.keys(req.session[key]).length === 0) { 58 | delete req.session[key]; 59 | } 60 | 61 | if (state !== req.query.state) { 62 | return this.fail({ 63 | message: 'Invalid authorization request state.' 64 | }, 403); 65 | } 66 | } 67 | 68 | var params = this.tokenParams(req, options); 69 | params.grant_type = 'authorization_code'; 70 | params.redirect_uri = callbackURL; 71 | this._oauth2.getOAuthAccessToken(code, params, 72 | function(err, accessToken, refreshToken, params) { 73 | if (err) { 74 | return self.error(self._createOAuthError('Failed to obtain access token', err)); 75 | } 76 | 77 | self._loadUserProfile(accessToken, params.openid, function(err, profile) { 78 | if (err) { 79 | return self.error(err); 80 | } 81 | 82 | function verified(err, user, info) { 83 | if (err) { 84 | return self.error(err); 85 | } 86 | if (!user) { 87 | return self.fail(info); 88 | } 89 | self.success(user, info); 90 | } 91 | 92 | try { 93 | if (self._passReqToCallback) { 94 | var arity = self._verify.length; 95 | if (arity == 6) { 96 | self._verify(req, accessToken, refreshToken, params, profile, verified); 97 | } else { // arity == 5 98 | self._verify(req, accessToken, refreshToken, profile, verified); 99 | } 100 | } else { 101 | var arity = self._verify.length; 102 | if (arity == 5) { 103 | self._verify(accessToken, refreshToken, params, profile, verified); 104 | } else { // arity == 4 105 | self._verify(accessToken, refreshToken, profile, verified); 106 | } 107 | } 108 | } catch (ex) { 109 | return self.error(ex); 110 | } 111 | }); 112 | } 113 | ); 114 | } else { 115 | var params = this.authorizationParams(req, options); 116 | params.response_type = 'code'; 117 | params.redirect_uri = callbackURL; 118 | var scope = options.scope || this._scope; 119 | if (scope) { 120 | if (Array.isArray(scope)) { 121 | scope = scope.join(this._scopeSeparator); 122 | } 123 | params.scope = scope; 124 | } 125 | var state = options.state; 126 | if (state) { 127 | params.state = state; 128 | } else if (this._state) { 129 | if (!req.session) { 130 | return this.error(new Error('OAuth2Strategy requires session support when using state. Did you forget app.use(express.session(...))?')); 131 | } 132 | 133 | var key = this._key; 134 | state = uid(24); 135 | if (!req.session[key]) { 136 | req.session[key] = {}; 137 | } 138 | req.session[key].state = state; 139 | params.state = state; 140 | } 141 | 142 | var location = this._oauth2.getAuthorizeUrl(params); 143 | this.redirect(location); 144 | } 145 | }; 146 | 147 | exports._loadUserProfile = function(accessToken, openid, done) { 148 | var self = this; 149 | 150 | function loadIt() { 151 | return self.userProfile(accessToken, openid, done); 152 | } 153 | 154 | function skipIt() { 155 | return done(null); 156 | } 157 | 158 | if (typeof this._skipUserProfile == 'function' && this._skipUserProfile.length > 1) { 159 | // async 160 | this._skipUserProfile(accessToken, openid, function(err, skip) { 161 | if (err) { 162 | return done(err); 163 | } 164 | if (!skip) { 165 | return loadIt(); 166 | } 167 | return skipIt(); 168 | }); 169 | } else { 170 | var skip = (typeof this._skipUserProfile == 'function') ? this._skipUserProfile() : this._skipUserProfile; 171 | if (!skip) { 172 | return loadIt(); 173 | } 174 | return skipIt(); 175 | } 176 | }; 177 | --------------------------------------------------------------------------------