├── .gitignore ├── LICENSE ├── README.md ├── example ├── app.js ├── package.json └── views │ ├── account.ejs │ ├── index.ejs │ └── layout.ejs ├── lib └── passport-tqq │ ├── index.js │ └── strategy.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Node.js 5 | node_modules 6 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011-2013 Heroic Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passport-TQQ 2 | 3 | 适用于[Passport](http://passportjs.org/)的[Tencent QQ](http://www.qq.com/)登录认证策略。 4 | 5 | ## Install 6 | 7 | $ npm install passport-tqq 8 | 9 | ## Usage 10 | 11 | #### Configure Strategy 12 | ``` 13 | passport.use(new TqqStrategy({ 14 | clientID: QQ_APP_ID, 15 | clientSecret: QQ_APP_KEY, 16 | callbackURL: "http://localhost:3000/auth/qq/callback" 17 | }, 18 | function(accessToken, refreshToken, profile, done) { 19 | User.findOrCreate({ qqId: profile.id }, function (err, user) { 20 | return done(err, user); 21 | }); 22 | } 23 | )); 24 | ``` 25 | #### Authenticate Requests 26 | ``` 27 | // QQ登录认证时 `state` 为必填参数 28 | // 系client端的状态值,用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回 29 | app.get('/auth/qq', function (req, res, next) { 30 | req.session = req.session || {}; 31 | req.session.authState = crypto.createHash('sha1') 32 | .update(-(new Date()) + '') 33 | .digest('hex'); 34 | passport 35 | .authenticate('qq', { 36 | state: req.session.authState 37 | })(req, res, next); 38 | }); 39 | 40 | app.get('/auth/qq/callback', function (req, res, next) { 41 | // 通过比较认证返回的`state`状态值与服务器端`session`中的`state`状态值 42 | // 决定是否继续本次授权 43 | if(req.session && req.session.authState 44 | && req.session.authState === req.query.state) { 45 | passport 46 | .authenticate('qq', { 47 | failureRedirect: '/login' 48 | })(req, res, next); 49 | } else { 50 | return next(new Error('Auth State Mismatch')); 51 | } 52 | }, 53 | function(req, res) { 54 | res.redirect('/'); 55 | }); 56 | ``` 57 | #### Extended Permissions 58 | 59 | 可以配置用户授权时向用户显示的可进行授权的列表。 60 | 61 | ``` 62 | app.get('/auth/qq', 63 | passport.authenticate('qq', { 64 | state: 'random state value', 65 | scope: ['get_user_info', 'list_album'] 66 | })); 67 | ``` 68 | 69 | ## Examples 70 | 71 | 见 [https://github.com/heroicyang/passport-tqq/tree/master/example](https://github.com/heroicyang/passport-tqq/tree/master/example) 72 | 73 | ## Credits 74 | 75 | - [Heroic Yang](http://github.com/heroicyang) 76 | 77 | ## License 78 | 79 | [The MIT License](http://opensource.org/licenses/MIT) 80 | 81 | Copyright (c) 2011-2013 Heroic Yang <[http://heroicyang.com/](http://heroicyang.com/)> -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , passport = require('passport') 3 | , util = require('util') 4 | , crypto = require('crypto') 5 | , TqqStrategy = require('../lib/passport-tqq/').Strategy; 6 | 7 | var QQ_APP_ID = 'blabla...' 8 | var QQ_APP_KEY = 'blabla...'; 9 | 10 | passport.serializeUser(function(user, done) { 11 | done(null, user); 12 | }); 13 | 14 | passport.deserializeUser(function(obj, done) { 15 | done(null, obj); 16 | }); 17 | 18 | passport.use(new TqqStrategy({ 19 | clientID: QQ_APP_ID, 20 | clientSecret: QQ_APP_KEY, 21 | callbackURL: 'http://127.0.0.1:3000/auth/qq/callback' 22 | }, 23 | function(accessToken, refreshToken, profile, done) { 24 | // asynchronous verification, for effect... 25 | process.nextTick(function () { 26 | return done(null, profile); 27 | }); 28 | } 29 | )); 30 | 31 | var app = express(); 32 | 33 | // configure Express 34 | app.configure(function() { 35 | app.set('views', __dirname + '/views'); 36 | app.set('view engine', 'ejs'); 37 | app.use(express.logger()); 38 | app.use(express.cookieParser()); 39 | app.use(express.bodyParser()); 40 | app.use(express.methodOverride()); 41 | app.use(express.session({ secret: 'keyboard cat' })); 42 | 43 | app.use(passport.initialize()); 44 | app.use(passport.session()); 45 | app.use(app.router); 46 | }); 47 | 48 | app.get('/', function(req, res){ 49 | res.render('index', { user: req.user }); 50 | }); 51 | 52 | app.get('/account', ensureAuthenticated, function(req, res){ 53 | res.render('account', { user: req.user }); 54 | }); 55 | 56 | // GET /auth/qq 57 | // QQ登录认证时 `state` 为必填参数 58 | // 系client端的状态值,用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回 59 | app.get('/auth/qq', function (req, res, next) { 60 | req.session = req.session || {}; 61 | req.session.authState = crypto.createHash('sha1') 62 | .update(-(new Date()) + '') 63 | .digest('hex'); 64 | passport.authenticate('qq', { 65 | state: req.session.authState 66 | })(req, res, next); 67 | }); 68 | 69 | // GET /auth/qq/callback 70 | // 通过比较认证返回的`state`状态值与服务器端`session`中的`state`状态值 71 | // 决定是否继续本次授权 72 | app.get('/auth/qq/callback', function (req, res, next) { 73 | if(req.session && req.session.authState 74 | && req.session.authState === req.query.state) { 75 | passport 76 | .authenticate('qq', { 77 | failureRedirect: '/' 78 | })(req, res, next); 79 | } else { 80 | return next(new Error('Auth State Mismatch')); 81 | } 82 | }, 83 | function(req, res) { 84 | res.redirect('/'); 85 | }); 86 | 87 | app.get('/logout', function(req, res){ 88 | req.logout(); 89 | res.redirect('/'); 90 | }); 91 | 92 | require('http').createServer(app).listen(3000); 93 | 94 | function ensureAuthenticated(req, res, next) { 95 | if (req.isAuthenticated()) { return next(); } 96 | res.redirect('/'); 97 | } 98 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-tqq-example", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "express": "~3.2.x", 6 | "ejs": "~0.8.x", 7 | "passport": "~0.1.x" 8 | }, 9 | "engines" : { "node" : ">=0.6", "npm" : ">=1.0" } 10 | } -------------------------------------------------------------------------------- /example/views/account.ejs: -------------------------------------------------------------------------------- 1 | <% include layout %> 2 |

OpenID: <%= user.id %>

3 |

Nickname: <%= user.nickname %>

4 | 5 | -------------------------------------------------------------------------------- /example/views/index.ejs: -------------------------------------------------------------------------------- 1 | <% include layout %> 2 | <% if (!user) { %> 3 |

Welcome! Please log in.

4 | <% } else { %> 5 |

Hello, <%= user.nickname %>.

6 | <% } %> 7 | 8 | -------------------------------------------------------------------------------- /example/views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Passport-TQQ Example 6 | 7 | 8 | <% if (!user) { %> 9 |

10 | Home | 11 | Log In With QQ 12 |

13 | <% } else { %> 14 |

15 | Home | 16 | Account | 17 | Log Out 18 |

19 | <% } %> -------------------------------------------------------------------------------- /lib/passport-tqq/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Strategy = require('./strategy'); 5 | 6 | /** 7 | * Expose constructors. 8 | */ 9 | exports.Strategy = Strategy; -------------------------------------------------------------------------------- /lib/passport-tqq/strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var util = require('util') 5 | , querystring = require('querystring') 6 | , OAuth2Strategy = require('passport-oauth').OAuth2Strategy 7 | , InternalOAuthError = require('passport-oauth').InternalOAuthError; 8 | 9 | 10 | /** 11 | * `Strategy` constructor. 12 | * 13 | * The Tencent QQ authentication strategy authenticates requests by delegating to 14 | * Tencent QQ using the OAuth 2.0 protocol. 15 | * 16 | * Applications must supply a `verify` callback which accepts an `accessToken`, 17 | * `refreshToken` and service-specific `profile`, and then calls the `done` 18 | * callback supplying a `user`, which should be set to `false` if the 19 | * credentials are not valid. If an exception occured, `err` should be set. 20 | * 21 | * Options: 22 | * - `clientID` your Tencent QQ application's App ID 23 | * - `clientSecret` your Tencent QQ application's App Key 24 | * - `callbackURL` URL to which Tencent QQ will redirect the user after granting authorization 25 | * 26 | * Examples: 27 | * 28 | * passport.use(new TqqStrategy({ 29 | * clientID: '123-456-789', 30 | * clientSecret: 'shhh-its-a-secret' 31 | * callbackURL: 'https://www.example.net/auth/qq/callback' 32 | * }, 33 | * function(accessToken, refreshToken, profile, done) { 34 | * User.findOrCreate(..., function (err, user) { 35 | * done(err, user); 36 | * }); 37 | * } 38 | * )); 39 | * 40 | * @param {Object} options 41 | * @param {Function} verify 42 | * @api public 43 | */ 44 | function Strategy(options, verify) { 45 | options = options || {}; 46 | options.authorizationURL = options.authorizationURL || 'https://graph.qq.com/oauth2.0/authorize'; 47 | options.tokenURL = options.tokenURL || 'https://graph.qq.com/oauth2.0/token'; 48 | options.scopeSeparator = options.scopeSeparator || ','; 49 | 50 | OAuth2Strategy.call(this, options, verify); 51 | this.name = 'qq'; 52 | 53 | var _oauth2_get = this._oauth2.get.bind(this._oauth2); 54 | this._oauth2.get = function (url, access_token, callback) { 55 | var extraQueryStr = querystring.stringify({ 56 | oauth_consumer_key: this._clientId 57 | }); 58 | url += (url.indexOf('?') === -1 ? '?' : '&') + extraQueryStr; 59 | _oauth2_get(url, access_token, callback); 60 | }; 61 | } 62 | 63 | /** 64 | * Inherit from `OAuth2Strategy`. 65 | */ 66 | util.inherits(Strategy, OAuth2Strategy); 67 | 68 | /** 69 | * Return extra QQ-specific parameters to be included in the authorization request. 70 | * 71 | * Options: 72 | * - `state` Client-side state value. For third-party applications to prevent CSRF attacks. 73 | * 74 | * @param {Object} options 75 | * @return {Object} 76 | * @api protected 77 | */ 78 | Strategy.prototype.authorizationParams = function (options) { 79 | if(!options.state) { 80 | throw new Error('Authentication Parameter `state` Required'); 81 | } else { 82 | return options; 83 | } 84 | }; 85 | 86 | /** 87 | * Retrieve user profile from QQ. 88 | * 89 | * This function constructs a normalized profile, with the following properties: 90 | * 91 | * - `provider` always set to `tqq` 92 | * - `id` the user's OpenID 93 | * - `nickname` the user's QZone nickname 94 | * - `gender` the user's gender 95 | * 96 | * @param {String} accessToken 97 | * @param {Function} done 98 | * @api protected 99 | */ 100 | Strategy.prototype.userProfile = function(accessToken, done) { 101 | var oauth2 = this._oauth2 102 | , openIDURL = 'https://graph.qq.com/oauth2.0/me' 103 | , profileURL = 'https://graph.qq.com/user/get_user_info' 104 | , openID; 105 | 106 | oauth2.get(openIDURL, accessToken, function (err, result, res) { 107 | if (err) { 108 | return done(new InternalOAuthError('failed to fetch user profile', err)); 109 | } 110 | 111 | try { 112 | openID = JSON.parse(result.match(/\{.*\}/)[0]).openid; 113 | profileURL += '?openid=' + openID; 114 | } catch (e) { 115 | return done(e); 116 | } 117 | 118 | oauth2.get(profileURL, accessToken, function (err, result, res) { 119 | try { 120 | var json = JSON.parse(result) 121 | , profile = { provider: 'qq' }; 122 | 123 | profile.id = openID; 124 | profile.nickname = json.nickname; 125 | profile.gender = json.gender; 126 | profile._raw = result; 127 | profile._json = json; 128 | 129 | done(null, profile); 130 | } catch (e) { 131 | done(e); 132 | } 133 | }); 134 | }); 135 | }; 136 | 137 | /** 138 | * Expose `Strategy`. 139 | */ 140 | module.exports = Strategy; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-tqq", 3 | "version": "0.0.2", 4 | "description": "Tencent QQ authentication strategy for Passport.", 5 | "keywords": ["passport", "qq", "tencent", "auth", "oauth2", "oauth", "authentication"], 6 | "author": { 7 | "name": "Heroic Yang", 8 | "email": "me@heroicyang.com", 9 | "url": "http://heroicyang.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:heroicyang/passport-tqq.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/heroicyang/passport-tqq/issues" 17 | }, 18 | "main": "./lib/passport-tqq", 19 | "dependencies": { 20 | "passport-oauth": "~0.1.x" 21 | }, 22 | "engines": { 23 | "node": ">=0.6", 24 | "npm": ">=1.0" 25 | }, 26 | "licenses": [{ 27 | "type": "MIT", 28 | "url": "http://www.opensource.org/licenses/MIT" 29 | }] 30 | } 31 | --------------------------------------------------------------------------------