Skip to content

Commit

Permalink
feat: add passport plugin (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 authored Feb 21, 2017
1 parent 92e16b4 commit 1201f5b
Show file tree
Hide file tree
Showing 23 changed files with 840 additions and 75 deletions.
1 change: 0 additions & 1 deletion .autod.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ module.exports = {
],
devdep: [
'egg',
'egg-ci',
'egg-bin',
'autod',
'eslint',
Expand Down
11 changes: 10 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
sudo: false
language: node_js
node_js:
- '4'
- '6'
- '7'
install:
Expand All @@ -10,3 +9,13 @@ script:
- npm run ci
after_script:
- npminstall codecov && codecov
env:
global:
# EGG_PASSPORT_TWITTER_CONSUMER_SECRET
- secure: "aNsMN4elXkDQ1rvikWW3wOSMzVj5G7VLBhVJWq2yj7Om+tfSMn+uzZppnBhHuYR6PjWu9UEliuk+EdF2nrdaFwb1LY3yfUwyqkDCj1/UXVPewchx2W4vwvWw0zKanE5UFMou8zNq44Gm3PWdeE3EcqeNN4qJb6i6kqnJTMBn+bxij0wRULiv4NnvUCliroSvBLu6bW3gE4Il8kqSXw5kJ+4haFF6kYFW5bjrg+zHB13zBrfv5KBdwiMtHsjsOFoHhaR/ijWaPLGpBVja19OnDrsMvWL0TLiEmyqCmECXrmL4bMPSby4w6Chx8OJNhIzhH1OgMYy7kucJn3s3x3rAZNNtLENsSI+VC4zh0spAt9gbdF4WCltVme9BNVOYy2lbfjUsvKmh+4WaX7F/yNITIq7CvaNH/7Fw+9c2BY4Blq846Qt0lDfqEczr9eqrRVUQrFVlElXWV/voGWChpv8ce+uYbjrRpf9ajOBBZy3080gwj5bEnu/Xx7bSjXILZv4mbbkGXYuMEU6fYdnMd19+UxpP2H21cGqmlCd0jL9r+zPsRGT9uu34uh8t5ASsiGnWhXAsk+i83ZxUXN6eXn5i3gQdYuATUNPFcyJLNgQZ1XNKQHavnNqf80f6KU8ppCiUNbAl5u+afzsv92JD64L26amBoFIq1mD83cMUmKjdB1Y="
# EGG_PASSPORT_WEIBO_CLIENT_ID
- secure: "dmar092ZvqXn/MVsH96s/upjgaIy3lRZEzQM5jnDekedMDbR2ZQs8N5IhE6w97JagGL4hm1TF/ZSOrmuFLU/WeOfArEkrKbJYaSRU43E49hiaW63/j9JwNEB0YXfwNgs2f5gr7FIw2/d/NUitheqnPbaamo+2l8gt3W/F04m/lDxlVoFmtwMrWYA+K4g/OhgQwe0WSMQzEPJ74W48hdQF/eSsYFQSAx6iQu8oM6Kz9C5GWX75vXeI1RJglv61wH7TKnj+YJY6RnW/VVxRmFu0utc6nSOaxDoncX9R8CuCYcj/HDrsF0K006ph/nIsDwDEtyA62KJYi/fluowUCeBIvleFj7r/c1Yx2O35TIEYsJNOBZWn+oeWmni+7htYVRYX7zKFIy6AENYDXWIVJe1tiCMeNTaTUUar/lF+UFu+sF7Z8p21izCD/gqYYvqWy0cZ/+Qb5tisyfzUWShMKhC/mrldb+bXtSzmSZfX7gP5JQeEuxdUF7HM3RHwaW+x1ORBjbvyZF5t1J3Pl095SGb/46wCLOkVmBsIvj0Gj+BcNWxaLro0IVyFF1SzjuWHcqHFx3RKe1puknIv8BKpoqMQCryBPsMRje09nXwbdP5L32ZpNjhf5i1lCVdzXGOAVfYa2GbuXoB+960SgFjmtxeRJP89QCyRFPRPV2MrUWvQPU="
# EGG_PASSPORT_TWITTER_CONSUMER_KEY
- secure: "a66F9RKyMDITxM3iEJA811mvHOY3wFgJFRVLU1uHz9BmmpUoqL9uKEhrcdJFgjfLTfxQfv6hk6HR4lg9MUyDf3NgF0U4KzC2guOHykMRd4NM7iVyO/FMMTkLiTd+QVYPAhqg9FsYEW9tLg2AJCU/pm7997zOJ0Qk9ibUCBWjrIKtZHPqcRWJ3dgXxSXc9WhinN5bbAzqj2GFXxNpGun1lHxNOzpF9g8p5R8crm2JEx1bywFzU1PxH1vKvF4EA37dCUnF4Qg1MTB1Wrdz5Agk7qda0kN/WCbWpcNAAWAwSAaAQKJemD28Hh52E6bj8ZmVerXJY0rzzk5utZxNOIAOKeBWWx+b9F2WRPiNnE4fsXos0ldwGGodOOM5MYfb3QSqDANQoMV+KTu26NV/Sy47epV0ohSc8L9Vg/pgUzPIxDMHdZgODP6UNRWeyA/Vi4TC04bCJQsIxxqxMk9rDe4nI7dz7nf33oEYl4BGrGBWCGkysAEo8Vz31ZPM6rDjXhmxmH+CtbWoFWnvqNxsEePjJl3tDXxBlzoqhSiHau+Z+Du8WdAOjZFWOuby46vmJU1a3DwdPt3V2aIhLLV1Mln2RcU11E7Mi13KKalcTugFsi57XaeJP30pcqkA9tRgozCxWXdoThQItVgu3ikCCpJPf2MUu9ciCN+Cl65LXLIMq5c="
# EGG_PASSPORT_WEIBO_CLIENT_SECRET
- secure: "FvjTYNVg+0H4+AJzZeZDQu2uu52kbdGfpKWnIh4owQXBGT3T3nIfzbCZPpDVaJcv/JyGSzkP4WuH/KvxuLu1ZzgRTu1r48rAIiuBcmQDzlSUQ0e5Gyfj3bwCASPgrkXTc6vC/SwvWIsAf6NAPwkS/NMaP1R71IJ0RSLs04JdU4zmoLTD6Yt896Tjf99MtAzqY+VfvLh8W4v5fa7a3WoNeXJW46deqh0yV/vQbH4HwbAipExTSMUlMk1xK+s9wqiafJEepU2wvIQps2sOLTytDV+6BDYKjyVEjs3cRJkpa8gFo16aOs09r3wuwtd5QJUQTTbOYhCTuQ6DKbxQHvOoT2Bw9uyP/jVF2xTPGxLOupn7cis+tgj/bmke/mJrZtb3+uZaKNXjGUXhz7a9mYWQgN+8hOVM2ia5goOWxGEMcaEbk5jir9XQRjyYeljfbyT3KXgAL8k61cYdPCGqXfvZVpYJbPHMOAQ+lyaV8TEqmPgJewWDUvhINc7QLmZCY5NwbqabAhYeKqIZG9oG/17gKBwDSDHtzTRN5Y//LQuctMBRSQN1tO0bw/ND0tOEGI71uXCwN/xMDC/vMAr0FqkFb2gH+hxAIrSM8/e6fUm0gMog0YzVwJtvEqSTRlrSB/bKsy7e/3Jf1HiiMvqDmPWTW/P8+zBcxJ74Ez8Joe02vr0="
217 changes: 186 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,56 +40,211 @@ exports.passport = {
};
```

### Using github and twitter strategy
### Using Github and Twitter strategy

```js
// app.js
// config/config.default.js
exports.passportGithub = {
key: 'my oauth2 clientID',
secret: 'my oauth2 clientSecret',
};

exports.passportTwitter: {
key: 'my oauth1 consumerKey',
secret: 'my oauth1 consumerSecret',
};
```

### Authenticate Requests

const GithubStrategy = require('passport-github2').Strategy;
const TwitterStrategy = require('passport-twitter').Strategy;
Use `app.passport.mount(strategy[, options])`, specifying the `'github'` and `'twitter'` strategy, to authenticate requests.

```js
// app/router.js
module.exports = app => {
const githubStrategy = new GithubStrategy({
consumerKey: app.config.github.consumerKey,
consumerSecret: app.config.github.consumerSecret,
// authURL: '/passport/auth/github',
// callbackURL: '/passport/auth/github/callback',
// scope: [ 'user:email' ],
}, function* (accessToken, refreshToken, profile, done) {
const user = yield User.findOrCreate(...);
// user must contains `id` property
return user;
app.get('/', 'home.index');

// authenticates routers
app.passport.mount('github');
// this is a passport router helper, it's equal to the below codes
//
// const github = app.passport.authenticate('github');
// app.get('/passport/github', github);
// app.get('/passport/github/callback', github);

// custom options.login url and options.successRedirect
app.passport.mount('twitter', {
loginURL: '/account/twitter',
// auth success redirect to /
successRedirect: '/',
});
};
```

const twitterStrategy = new TwitterStrategy({
consumerKey: app.config.twitter.consumerKey,
consumerSecret: app.config.twitter.consumerSecret,
// authURL: '/passport/auth/twitter',
// callbackURL: '/passport/auth/twitter/callback',
}, function* (token, tokenSecret, profile, done) {
const user = yield User.findOrCreate(...);
// user must contains `id` property
return user;
});
### Verify and store user

Use `app.passport.verify(function* (ctx, user) {})` hook:

app.passport.use(githubStrategy);
app.passport.use(twitterStrategy);
```js
// app.js
module.exports = app => {
app.passport.verify(function* (ctx, user) {
// check user
assert(user.provider, 'user.provider should exists');
assert(user.id, 'user.id should exists');

// find user from database
//
// Authorization Table
// column | desc
// --- | --
// provider | provider name, like github, twitter, facebook, weibo and so on
// uid | provider unique id
// user_id | current application user id
const auth = yield ctx.model.Authorization.findOne({
uid: user.id,
provider: user.provider,
});
const existsUser = yield ctx.model.User.findOne({ id: auth.user_id });
if (existsUser) {
return existsUser;
}
// call user service to register a new user
const newUser = yield ctx.service.user.register(user);
return newUser;
});
};
```

### Authenticate Requests
## How to develop an `egg-passport-${provider}` plugin

See example: [egg-passport-twitter](https://github.com/eggjs/egg-passport-twitter).

Use `app.passport.authenticate()`, specifying the `'github'` and `'twitter'` strategy, to authenticate requests.
- Plugin dependencies on [egg-passport](https://github.com/eggjs/egg-passport) to use `app.passport` APIs.

```json
// package.json
{
"eggPlugin": {
"name": "passportTwitter",
"dependencies": [
"passport"
]
},
}
```

- Define config and set default values

**Must use `key` and `secret` instead of `consumerKey|clientID` and `consumerSecret|clientSecret`.**

```js
// app/router.js
// config/config.default.js
exports.passportTwitter: {
key: '',
secret: '',
callbackURL: '/passport/twitter/callback',
};
```

- Init `Strategy` in `app.js` and format user in `verify callback`

```js
// app.js
const debug = require('debug')('egg-passport-twitter');
const assert = require('assert');
const Strategy = require('passport-twitter').Strategy;

module.exports = app => {
app.get('/passport/auth/github', app.passport.authenticate('github'));
app.get('/passport/auth/twitter', app.passport.authenticate('twitter'));
const config = app.config.passportTwitter;
// must set passReqToCallback to true
config.passReqToCallback = true;
assert(config.key, '[egg-passport-twitter] config.passportTwitter.key required');
assert(config.secret, '[egg-passport-twitter] config.passportTwitter.secret required');
// convert to consumerKey and consumerSecret
config.consumerKey = config.key;
config.consumerSecret = config.secret;

// register twitter strategy into `app.passport`
// must require `req` params
app.passport.use('twitter', new Strategy(config, (req, token, tokenSecret, params, profile, done) => {
// format user
const user = {
provider: 'twitter',
id: profile.id,
name: profile.username,
displayName: profile.displayName,
photo: profile.photos && profile.photos[0] && profile.photos[0].value,
token,
tokenSecret,
params,
profile,
};
debug('%s %s get user: %j', req.method, req.url, user);
// let passport do verify and call verify hook
app.passport.doVerify(req, user, done);
}));
};
```

- That's all!

## APIs

### extent `application`

- `app.passport.mount(strategy, options)`: Mount the login and the login callback routers to use the given `strategy`.
- `app.passport.authenticate(strategy, options)`: Create a middleware that will authorize a third-party account using the given `strategy` name, with optional `options`.
- `app.passport.verify(handler)`: Verify authenticated user
- `app.passport.serializeUser(handler)`: Serialize user before store into session
- `app.passport.deserializeUser(handler)`: Deserialize user after restore from session

### extend `context`

- `ctx.user`: get the current authenticated user
- `ctx.isAuthenticated()`: Test if request is authenticated
- `* ctx.login(user[, options])`: Initiate a login session for `user`.
- `ctx.logout()`: Terminate an existing login session

## Unit Tests

This plugin has includes some mock methods to helper you writing unit tests more conveniently.

### `app.mockUser([user])`: Mock an authenticated user

```js
const mm = require('egg-mock');

describe('mock user demo', () => {
let app;
before(() => {
app = mm.app();
return app.ready();
});
after(() => app.close());

afterEach(mm.restore);

it('should show authenticated user info', () => {
app.mockUser();
return request(app.callback())
.get('/')
.expect(/user name: mock_name/)
.expect(200);
});
});
```

### `app.mockUserContext([user])`: Mock a context instance with authenticated user

```js
it('should get authenticated user and call service', function* () {
const ctx = app.mockUserContext();
const result = yield ctx.service.findUser({ id: ctx.user.id });
assert(result.user.id === ctx.user.id);
});
```

## Questions & Suggestions

Please open an issue [here](https://github.com/eggjs/egg/issues).
Expand Down
6 changes: 4 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use strict';

const KoaPassport = require('./lib/passport');
const assert = require('assert');
const Passport = require('./lib/passport');

module.exports = app => {
app.passport = new KoaPassport();
app.passport = new Passport(app);

assert(app.config.coreMiddleware.includes('session'), '[egg-passport] session middleware must exists');
app.config.coreMiddleware.push('passportInitialize');
app.config.coreMiddleware.push('passportSession');
};
54 changes: 54 additions & 0 deletions app/extend/application.unittest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';

module.exports = {
/**
* mock an authenticated user
*
* @param {Object} [user] - mock user data
*/
mockUser(user) {
user = Object.assign({
provider: 'mock',
id: '10086',
name: 'mock_name',
displayName: 'mock displayName',
photo: 'https://tva2.sinaimg.cn/crop.0.0.180.180.180/61c56ebcjw1e8qgp5bmzyj2050050aa8.jpg',
profile: {
photos: [
{ value: 'http://tva2.sinaimg.cn/crop.0.0.180.180.180/61c56ebcjw1e8qgp5bmzyj2050050aa8.jpg' },
],
_raw: '{}',
_json: {
id: '10086',
screen_name: 'mock_name',
displayName: 'mock displayName',
},
},
}, user);

const createContext = this.createContext;
this.mm(this, 'createContext', (req, res) => {
req.user = user;
const ctx = createContext.call(this, req, res);
return ctx;
});
},

/**
* mock a context instance with authenticated user
*
* @param {Object} [user] - mock user data
* @return {Context} ctx - context instance
*/
mockUserContext(user) {
this.mockUser(user);
const ctx = this.mockContext();
// ctx.req is not the http request
// login, logout, isAuthenticated, isUnauthenticated
ctx.req.login = () => Promise.resolve();
ctx.req.logout = () => {};
ctx.req.isAuthenticated = () => true;
ctx.req.isUnauthenticated = () => false;
return ctx;
},
};
48 changes: 48 additions & 0 deletions app/extend/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

module.exports = {
get user() {
return this.req[this.app.passport._userProperty];
},

// https://github.com/jaredhanson/passport/blob/master/lib/http/request.js
// proxy login, logout, isAuthenticated, isUnauthenticated to ctx.req

/**
* Initiate a login session for `user`.
*
* @param {Object} user - authenticated user
* @param {Object} [options] - login options
* - {Boolean} options.session - Save login state in session, defaults to true
* @return {Promise} success or not promise instance
*
* @api public
*/
login(user, options) {
return new Promise((resolve, reject) => {
this.req.login(user, options, err => {
if (err) return reject(err);
resolve();
});
});
},

/**
* Terminate an existing login session.
*
* @api public
*/
logout(...args) {
this.req.logout(...args);
},

/**
* Test if request is authenticated.
*
* @return {Boolean} - if authenticated return true
* @api public
*/
isAuthenticated() {
return this.req.isAuthenticated();
},
};
Loading

0 comments on commit 1201f5b

Please sign in to comment.