diff --git a/Changelog.md b/Changelog.md index 54d34410e..dfb6c6f35 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,22 @@ ## Changelog +### 2.4.0 + +- Set Cache-Control and Pragma headers +- Allow any valid URI for extension grants +- Expose `client` to `extendedGrant` and after via `req.oauth.client` +- Fix express depreciation warning for `res.send()` +- Expose `user` to `generateToken` and after via `req.user` +- Fix lockdown pattern for express 3 + +- Add redis example +- Fix docs to use new express bodyParser module +- Fix docs for `redirect_uri` +- Clarify docs for `clientIdRegex` +- Fix docs for missing `req` argument in `generateToken` +- Fix docs for `user`/`userId` `getAccessToken` +- Fix docs for argument order in `getRefreshToken` + ### 2.3.0 - Support "state" param for auth_code grant type diff --git a/Readme.md b/Readme.md index 772e27856..e2e056efd 100644 --- a/Readme.md +++ b/Readme.md @@ -55,7 +55,7 @@ Note: As no model was actually implemented here, delving any deeper, i.e. passin - grant types you wish to support, currently the module supports `password` and `refresh_token` - Default: `[]` - *function|boolean* **debug** - - If `true` errors will be logged to console. You may also pass a custom function, in which case that function will be called with the error as it's first argument + - If `true` errors will be logged to console. You may also pass a custom function, in which case that function will be called with the error as its first argument - Default: `false` - *number* **accessTokenLifetime** - Life of access tokens in seconds @@ -69,8 +69,8 @@ Note: As no model was actually implemented here, delving any deeper, i.e. passin - Life of auth codes in seconds - Default: `30` - *regexp* **clientIdRegex** - - Regex to match auth codes against before checking model - - Default: `/^[a-z0-9-_]{3,40}$/i` + - Regex to sanity check client id against before checking model. Note: the default just matches common `client_id` structures, change as needed + - Default: `/^[a-z0-9-_]{3,40}$/i` - *boolean* **passthroughErrors** - If true, **non grant** errors will not be handled internally (so you can ensure a consistent format with the rest of your api) - *boolean* **continueAfterResponse** @@ -97,8 +97,9 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *date* **expires** - The date when it expires - `null` to indicate the token **never expires** - - *string|number* **userId** - - The user id (saved in req.user.id) + - *mixed* **user** *or* *string|number* **userId** + - If a `user` key exists, this is saved as `req.user` + - Otherwise a `userId` key must exist, which is saved in `req.user.id` #### getClient (clientId, clientSecret, callback) - *string* **clientId** @@ -112,6 +113,7 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - Saved in `req.client` - Must contain the following keys: - *string* **clientId** + - *string* **redirectUri** (`authorization_code` grant type only) #### grantTypeAllowed (clientId, grantType, callback) - *string* **clientId** @@ -247,9 +249,11 @@ The spec does not actually require that you revoke the old token - hence this is ### Optional -#### generateToken (type, callback) +#### generateToken (type, req, callback) - *string* **type** - `accessToken` or `refreshToken` +- *object* **req** + - The current express request - *function* **callback (error, token)** - *mixed* **error** - Truthy to indicate an error @@ -262,7 +266,7 @@ The spec does not actually require that you revoke the old token - hence this is ## Extension Grants You can support extension/custom grants by implementing the extendedGrant method as outlined above. -Any requests that begin with http(s):// (as [defined in the spec](http://tools.ietf.org/html/rfc6749#section-4.5)) will be passed to it for you to handle. +Any grant type that is a valid URI will be passed to it for you to handle (as [defined in the spec](http://tools.ietf.org/html/rfc6749#section-4.5)). You can access the grant type via the first argument and you should pass back supported as `false` if you do not support it to ensure a consistent (and compliant) response. ## Example using the `password` grant type diff --git a/examples/dynamodb/index.js b/examples/dynamodb/index.js index e080c86cd..059d7af6a 100644 --- a/examples/dynamodb/index.js +++ b/examples/dynamodb/index.js @@ -4,7 +4,9 @@ var express = require('express'), var app = express(); -app.use(bodyParser()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.use(bodyParser.json()); app.oauth = oauthserver({ model: require('./model'), diff --git a/examples/postgresql/index.js b/examples/postgresql/index.js index e1cf18e5e..1c9af863f 100644 --- a/examples/postgresql/index.js +++ b/examples/postgresql/index.js @@ -4,7 +4,9 @@ var express = require('express'), var app = express(); -app.use(bodyParser()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.use(bodyParser.json()); app.oauth = oauthserver({ model: require('./model'), diff --git a/examples/redis/README.md b/examples/redis/README.md new file mode 100644 index 000000000..87c17951a --- /dev/null +++ b/examples/redis/README.md @@ -0,0 +1,19 @@ +# Redis Example + +A simple example with support for `password` and `refresh_token` grants using [Redis](http://redis.io/). You'll need [node-redis](https://github.com/mranney/node_redis) installed. + +## Usage + +```js +app.oauth = oauthserver({ + model: require('./model'), + grants: ['password', 'refresh_token'], + debug: true +}); +``` + +## Data model + +The example makes use of a simple data model where clients, tokens, refresh tokens and users are stored as [hashes](http://redis.io/topics/data-types#hashes). The allowed grants for each client are stored in a [set](http://redis.io/topics/data-types#sets) `clients:{id}:grant_types`. This allows grants to be added or removed dynamically. To simplify the user lookup users are identified by their username and not by a separate ID. Passwords are stored in the clear for simplicity, but in practice these should be hashed using a library like [bcrypt](https://github.com/ncb000gt/node.bcrypt.js). + +To inject some test data you can run the `testData.js` script in this directory. This will create a client with the ID `client` and secret `secret` and create a single user with the username `username` and password `password`. \ No newline at end of file diff --git a/examples/redis/index.js b/examples/redis/index.js new file mode 100644 index 000000000..8422cac1e --- /dev/null +++ b/examples/redis/index.js @@ -0,0 +1,33 @@ +var express = require('express'), + bodyParser = require('body-parser'), + oauthserver = require('../../'); // Would be: 'oauth2-server' + +var app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); + +app.use(bodyParser.json()); + +app.oauth = oauthserver({ + model: require('./model'), + grants: ['password', 'refresh_token'], + debug: true +}); + +// Handle token grant requests +app.all('/oauth/token', app.oauth.grant()); + +app.get('/secret', app.oauth.authorise(), function (req, res) { + // Will require a valid access_token + res.send('Secret area'); +}); + +app.get('/public', function (req, res) { + // Does not require an access_token + res.send('Public area'); +}); + +// Error handling +app.use(app.oauth.errorHandler()); + +app.listen(3000); diff --git a/examples/redis/model.js b/examples/redis/model.js new file mode 100644 index 000000000..41eccf0e3 --- /dev/null +++ b/examples/redis/model.js @@ -0,0 +1,90 @@ +var model = module.exports, + util = require('util'), + redis = require('redis'); + +var db = redis.createClient(); + +var keys = { + token: 'tokens:%s', + client: 'clients:%s', + refreshToken: 'refresh_tokens:%s', + grantTypes: 'clients:%s:grant_types', + user: 'users:%s' +}; + +model.getAccessToken = function (bearerToken, callback) { + db.hgetall(util.format(keys.token, bearerToken), function (err, token) { + if (err) return callback(err); + + if (!token) return callback(); + + callback(null, { + accessToken: token.accessToken, + clientId: token.clientId, + expires: token.expires ? new Date(token.expires) : null, + userId: token.userId + }); + }); +}; + +model.getClient = function (clientId, clientSecret, callback) { + db.hgetall(util.format(keys.client, clientId), function (err, client) { + if (err) return callback(err); + + if (!client || client.clientSecret !== clientSecret) return callback(); + + callback(null, { + clientId: client.clientId, + clientSecret: client.clientSecret + }); + }); +}; + +model.getRefreshToken = function (bearerToken, callback) { + db.hgetall(util.format(keys.refreshToken, bearerToken), function (err, token) { + if (err) return callback(err); + + if (!token) return callback(); + + callback(null, { + refreshToken: token.accessToken, + clientId: token.clientId, + expires: token.expires ? new Date(token.expires) : null, + userId: token.userId + }); + }); +}; + +model.grantTypeAllowed = function (clientId, grantType, callback) { + db.sismember(util.format(keys.grantTypes, clientId), grantType, callback); +}; + +model.saveAccessToken = function (accessToken, clientId, expires, user, callback) { + db.hmset(util.format(keys.token, accessToken), { + accessToken: accessToken, + clientId: clientId, + expires: expires ? expires.toISOString() : null, + userId: user.id + }, callback); +}; + +model.saveRefreshToken = function (refreshToken, clientId, expires, user, callback) { + db.hmset(util.format(keys.refreshToken, refreshToken), { + refreshToken: refreshToken, + clientId: clientId, + expires: expires ? expires.toISOString() : null, + userId: user.id + }, callback); +}; + +model.getUser = function (username, password, callback) { + db.hgetall(util.format(keys.user, username), function (err, user) { + if (err) return callback(err); + + if (!user || password !== user.password) return callback(); + + callback(null, { + id: username + }); + }); +}; diff --git a/examples/redis/testData.js b/examples/redis/testData.js new file mode 100644 index 000000000..c12afa877 --- /dev/null +++ b/examples/redis/testData.js @@ -0,0 +1,27 @@ +#! /usr/bin/env node + +var db = require('redis').createClient(); + +db.multi() + .hmset('users:username', { + id: 'username', + username: 'username', + password: 'password' + }) + .hmset('clients:client', { + clientId: 'client', + clientSecret: 'secret' + }) + .sadd('clients:client:grant_types', [ + 'password', + 'refresh_token' + ]) + .exec(function (errs) { + if (errs) { + console.error(errs[0].message); + return process.exit(1); + } + + console.log('Client and user added successfully'); + process.exit(); + }); diff --git a/lib/authorise.js b/lib/authorise.js index 18a018bd4..caebd221c 100644 --- a/lib/authorise.js +++ b/lib/authorise.js @@ -14,7 +14,7 @@ * limitations under the License. */ -var error = require('node-restify-errors'), +var error = require('node-restify-errors'), runner = require('./runner'); module.exports = Authorise; @@ -50,83 +50,76 @@ function Authorise (config, req, next) { * * Extract token from request according to RFC6750 * - * @param {Function} next + * @param {Function} done * @this OAuth */ -function getBearerToken (next) { - var headerToken = this.req.authorization, - getToken = this.req.query.access_token, - postToken = this.req.body ? this.req.body.access_token : undefined; +function getBearerToken (done) { + var headerToken = this.req.headers.authorization, + getToken = this.req.query.access_token, + postToken = this.req.body ? this.req.body.access_token : undefined; // Check exactly one method was used - var methodsUsed = (headerToken !== undefined && Object.keys(headerToken).length > 0) + (getToken !== undefined) + - (postToken !== undefined); + var methodsUsed = (headerToken !== undefined) + (getToken !== undefined) + + (postToken !== undefined); if (methodsUsed > 1) { - return next(new error.BadMethodError('Only one method may be used to authenticate at a time (Auth header, GET or POST).')); + return done(new error.BadMethodError('Only one method may be used to authenticate at a time (Auth header, GET or POST).')); } else if (methodsUsed === 0) { - return next(new error.InvalidCredentialsError('The access token was not found')); + return done(new error.InvalidCredentialsError('The access token was not found')); } // Header: http://tools.ietf.org/html/rfc6750#section-2.1 - if (headerToken && headerToken.credentials) { - var matches = (headerToken.scheme === 'Bearer') ? headerToken.credentials : null; + if (headerToken) { + var matches = headerToken.match(/Bearer\s(\S+)/); if (!matches) { - return next(new error.InvalidHeaderError('Malformed auth header')); + return done(new error.InvalidHeaderError('Malformed auth header')); } - headerToken = matches; - } else { - headerToken = undefined; + headerToken = matches[1]; } // POST: http://tools.ietf.org/html/rfc6750#section-2.2 if (postToken) { if (this.req.method === 'GET') { - return next(new error.BadMethodError('Method cannot be GET When putting the token in the body.')); + return done(new error.BadMethodError('Method cannot be GET When putting the token in the body.')); } if (!this.req.is('application/x-www-form-urlencoded')) { - return next(new error.BadMethodError('When putting the token in the ' + + return done(new error.BadMethodError('When putting the token in the ' + 'body, content type must be application/x-www-form-urlencoded.')); } } this.bearerToken = headerToken || postToken || getToken; - next(); + done(); } /** * Check token * * Check it against model, ensure it's not expired - * @param {Function} next + * @param {Function} done * @this OAuth */ -function checkToken (next) { +function checkToken (done) { var self = this; + this.model.getAccessToken(this.bearerToken, function (err, token) { + if (err) return done(new error.InternalError(err)); - if (this.model && Object.keys(this.model).length > 0) { - this.model.getAccessToken(this.bearerToken, function (err, token) { - if (err) return next(new error.InternalError(err)); - - if (!token) { - return next(new error.InvalidCredentialsError('The access token provided is invalid.')); - } + if (!token) { + return done(new error.InvalidCredentialsError('The access token provided is invalid.')); + } - if (token.expires !== null && - (!token.expires || token.expires < new Date())) { - return next(new error.InvalidCredentialsError('The access token provided has expired.')); - } + if (token.expires !== null && + (!token.expires || token.expires < new Date())) { + return done(new error.InvalidCredentialsError('The access token provided has expired.')); + } - // Expose params - self.req.oauth = {bearerToken: token}; - self.req.user = token.user ? token.user : {id: token.userId}; + // Expose params + self.req.oauth = { bearerToken: token }; + self.req.user = token.user ? token.user : { id: token.userId }; - next(); - }); - } else { - next(new error.InvalidCredentialsError('The access token can not be find.')); - } -} + done(); + }); +} \ No newline at end of file diff --git a/lib/grant.js b/lib/grant.js index c15d3f5fe..f635b6ce1 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -31,6 +31,7 @@ var fns = [ checkClient, checkGrantTypeAllowed, checkGrantType, + exposeUser, generateAccessToken, saveAccessToken, generateRefreshToken, @@ -70,7 +71,7 @@ function extractCredentials (next) { } // Grant type - this.grantType = this.req.body && this.req.body.grant_type; + this.grantType = this.req.params && this.req.params.grant_type; if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) { return next(new error.BadMethodError('Invalid or missing grant_type parameter')); } @@ -119,7 +120,7 @@ function credsFromBasic (req) { * @return {Object} Client */ function credsFromBody (req) { - return new Client(req.body.client_id, req.body.client_secret); + return new Client(req.params.client_id, req.params.client_secret); } /** @@ -129,16 +130,19 @@ function credsFromBody (req) { * @this OAuth */ function checkClient (next) { + var self = this; this.model.getClient(this.client.clientId, this.client.clientSecret, - function (err, client) { - if (err) return next(new error.InternalError(err)); + function (err, client) { + if (err) return next(new error.InternalError(err)); - if (!client) { - return next(new error.InvalidCredentialsError('Client credentials are invalid')); - } + if (!client) { + return next(new error.InvalidCredentialsError('Client credentials are invalid')); + } - next(); - }); + // Expose validated client + self.req.oauth = { client: client }; + next(); + }); } /** @@ -148,7 +152,8 @@ function checkClient (next) { * @this OAuth */ function checkGrantType (next) { - if (this.grantType.match(/^http(s|):\/\//) && this.model.extendedGrant) { + if (this.grantType.match(/^[a-zA-Z][a-zA-Z0-9+.-]+:/) + && this.model.extendedGrant) { return useExtendedGrant.call(this, next); } @@ -172,7 +177,7 @@ function checkGrantType (next) { * @param {Function} next */ function useAuthCodeGrant (next) { - var code = this.req.body.code; + var code = this.req.params.code; if (!code) { return next(new error.MissingParameterError('No "code" parameter')); @@ -204,8 +209,8 @@ function useAuthCodeGrant (next) { */ function usePasswordGrant (next) { // User credentials - var uname = this.req.body.username, - pword = this.req.body.password; + var uname = this.req.params.username, + pword = this.req.params.password; if (!uname || !pword) { return next(new error.InvalidCredentialsError('Missing parameters. "username" and "password" are required')); } @@ -228,7 +233,7 @@ function usePasswordGrant (next) { * @param {Function} next */ function useRefreshTokenGrant (next) { - var token = this.req.body.refresh_token; + var token = this.req.params.refresh_token; if (!token) { return next(new error.BadMethodError('No "refresh_token" parameter')); @@ -333,6 +338,18 @@ function checkGrantTypeAllowed (next) { }); } +/** + * Expose user + * + * @param {Function} done + * @this OAuth + */ +function exposeUser (done) { + this.req.user = this.user; + + done(); +} + /** * Generate an access token * diff --git a/lib/oauth2server.js b/lib/oauth2server.js index 5123c8bc9..7d6a5ad86 100644 --- a/lib/oauth2server.js +++ b/lib/oauth2server.js @@ -66,7 +66,7 @@ OAuth2Server.prototype.authorise = function () { var self = this; return function (req, res, next) { - new Authorise(self, req, next); + return new Authorise(self, req, next); }; }; @@ -125,97 +125,6 @@ OAuth2Server.prototype.authCodeGrant = function (check) { }; }; -/** - * Lockdown - * - * When using the lockdown patter, this function should be called after - * all routes have been declared. - * It will search through each route and if it has not been explitly bypassed - * (by passing oauth.bypass) then authorise will be inserted. - * If oauth.grant has been passed it will replace it with the proper grant - * middleware - * NOTE: When using this method, you must PASS the method not CALL the method, - * e.g.: - * - * ` - * server.post('/oauth/token', app.oauth.grant); - * - * server.get('/secrets', function (req, res, next) { - * res.send('secrets'); - * next(); - * }); - * - * server.get('/public', server.oauth.bypass, function (req, res, next) { - * res.send('public'); - * next(); - * }); - * - * server.oauth.lockdown(server); - * ` - * - * @param {Object} server Restify server - */ -OAuth2Server.prototype.lockdown = function (server) { - var self = this; - - //var lockdownRestify = function (mount) { - // //console.log(mount); - //}; - - //var lockdownExpress3 = function (stack) { - // // Check if it's a grant route - // var pos = stack.indexOf(self.grant); - // if (pos !== -1) { - // stack[pos] = self.grant(); - // return; - // } - // - // // Check it's not been explitly bypassed - // pos = stack.indexOf(self.bypass); - // if (pos === -1) { - // stack.unshift(self.authorise()); - // } else { - // stack.splice(pos, 1); - // } - //}; - // - //var lockdownExpress4 = function (layer) { - // if (!layer.route) - // return; - // - // var stack = layer.route.stack; - // var handlers = stack.map(function (item) { - // return item.handle; - // }); - // - // // Check if it's a grant route - // var pos = handlers.indexOf(self.grant); - // if (pos !== -1) { - // stack[pos].handle = self.grant(); - // return; - // } - // - // // Check it's not been explitly bypassed - // pos = handlers.indexOf(self.bypass); - // if (pos === -1) { - // // Add authorise another route (could do it properly with express.route?) - // var copy = {}; - // var first = stack[0]; - // for (var key in first) { - // copy[key] = first[key]; - // } - // copy.handle = self.authorise(); - // stack.unshift(copy); - // } else { - // stack.splice(pos, 1); - // } - //}; - - //for (var i in server.router.mounts) { - // lockdownRestify(server.router.mounts[i]); - //} -}; - /** * Bypass * diff --git a/package.json b/package.json index 21260361e..4c8f393af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oauth2-server-restify", "description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with restify in node.js", - "version": "2.3.2", + "version": "2.4.0", "keywords": [ "oauth", "oauth2" @@ -18,7 +18,6 @@ ], "main": "lib/oauth2server.js", "dependencies": { - "node-oauth2-server-restify": "git+https://github.com/marsanla/node-oauth2-server-restify.git", "basic-auth": "~0.0.1", "node-restify-errors": "~0.1.0" }, diff --git a/test/error.js b/test/error.js index 061340585..f8f23a853 100644 --- a/test/error.js +++ b/test/error.js @@ -28,10 +28,23 @@ describe('OAuth2Error', function() { error.name.should.equal('OAuth2Error'); }); - it('should expose `headers` if error is `invalid_client`', function () { + it('should set cache `headers`', function () { + var error = new OAuth2Error('invalid_request'); + + error.headers.should.eql({ + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache' + }); + }); + + it('should include WWW-Authenticate `header` if error is `invalid_client`', function () { var error = new OAuth2Error('invalid_client'); - error.headers.should.eql({ 'WWW-Authenticate': 'Basic realm="Service"' }); + error.headers.should.eql({ + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + 'WWW-Authenticate': 'Basic realm="Service"' + }); }); it('should expose a status `code`', function () { diff --git a/test/grant.extended.js b/test/grant.extended.js index 6c6191118..f75c9bd65 100644 --- a/test/grant.extended.js +++ b/test/grant.extended.js @@ -121,17 +121,19 @@ describe('Granting with extended grant type', function () { var app = bootstrap({ model: { getClient: function (id, secret, callback) { - callback(false, true); + callback(false, { clientId: 'thom', clientSecret: 'nightworld' }); }, grantTypeAllowed: function (clientId, grantType, callback) { callback(false, true); }, extendedGrant: function (grantType, req, callback) { + req.oauth.client.clientId.should.equal('thom'); + req.oauth.client.clientSecret.should.equal('nightworld'); callback(false, true, { id: 3 }); }, - saveAccessToken: function () { - done(); // That's enough - } + saveAccessToken: function (token, clientId, expires, user, cb) { + cb(); + }, }, grants: ['http://custom.com'] }); @@ -146,4 +148,34 @@ describe('Granting with extended grant type', function () { }) .expect(200, done); }); + + it('should allow any valid URI valid request', function (done) { + var app = bootstrap({ + model: { + getClient: function (id, secret, callback) { + callback(false, true); + }, + grantTypeAllowed: function (clientId, grantType, callback) { + callback(false, true); + }, + extendedGrant: function (grantType, req, callback) { + callback(false, true, { id: 3 }); + }, + saveAccessToken: function (token, clientId, expires, user, cb) { + cb(); + }, + }, + grants: ['urn:custom:grant'] + }); + + request(app) + .post('/oauth/token') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ + grant_type: 'urn:custom:grant', + client_id: 'thom', + client_secret: 'nightworld' + }) + .expect(200, done); + }); }); diff --git a/test/grant.js b/test/grant.js index b6a323b61..2f345c4e6 100644 --- a/test/grant.js +++ b/test/grant.js @@ -252,6 +252,40 @@ describe('Grant', function() { }); + it('should include client and user in request', function (done) { + var app = bootstrap({ + model: { + getClient: function (id, secret, callback) { + callback(false, { clientId: 'thom', clientSecret: 'nightworld' }); + }, + grantTypeAllowed: function (clientId, grantType, callback) { + callback(false, true); + }, + getUser: function (uname, pword, callback) { + callback(false, { id: 1 }); + }, + generateToken: function (type, req, callback) { + req.oauth.client.clientId.should.equal('thom'); + req.oauth.client.clientSecret.should.equal('nightworld'); + req.user.id.should.equal(1); + callback(false, 'thommy'); + }, + saveAccessToken: function (token, clientId, expires, user, cb) { + token.should.equal('thommy'); + cb(); + } + }, + grants: ['password'] + }); + + request(app) + .post('/oauth/token') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(validBody) + .expect(200, /thommy/, done); + + }); + it('should reissue if model returns object', function (done) { var app = bootstrap({ model: { @@ -377,6 +411,8 @@ describe('Grant', function() { .set('Content-Type', 'application/x-www-form-urlencoded') .send(validBody) .expect(200) + .expect('Cache-Control', 'no-store') + .expect('Pragma', 'no-cache') .end(function (err, res) { if (err) return done(err); @@ -418,6 +454,8 @@ describe('Grant', function() { .set('Content-Type', 'application/x-www-form-urlencoded') .send(validBody) .expect(200) + .expect('Cache-Control', 'no-store') + .expect('Pragma', 'no-cache') .end(function (err, res) { if (err) return done(err); diff --git a/test/lockdown.js b/test/lockdown.js index 47a8b80b0..5e95e5870 100644 --- a/test/lockdown.js +++ b/test/lockdown.js @@ -20,6 +20,7 @@ var express = require('express'), should = require('should'); var oauth2server = require('../'); +var Authorise = require('../lib/authorise'); var bootstrap = function (oauthConfig) { var app = express(); @@ -86,4 +87,48 @@ describe('Lockdown pattern', function() { .get('/public') .expect(200, /hello/i, done); }); + + describe('in express 3', function () { + var app, privateAction, publicAction; + + beforeEach(function () { + privateAction = function () {}; + publicAction = function () {}; + + // mock express 3 app + app = { + routes: { get: [] } + }; + + app.oauth = oauth2server({ model: {} }); + app.routes.get.push({ callbacks: [ privateAction ] }); + app.routes.get.push({ callbacks: [ app.oauth.bypass, publicAction ] }) + app.oauth.lockdown(app); + }); + + function mockRequest(authoriseFactory) { + var req = { + get: function () {}, + query: { access_token: { expires: null } } + }; + var next = function () {}; + + app.oauth.model.getAccessToken = function (t, c) { c(null, t); }; + + return authoriseFactory(req, null, next); + } + + it('adds authorise to non-bypassed routes', function () { + var authorise = mockRequest(app.routes.get[0].callbacks[0]); + authorise.should.be.an.instanceOf(Authorise); + }); + + it('runs non-bypassed routes after authorise', function () { + app.routes.get[0].callbacks[1].should.equal(privateAction); + }); + + it('removes oauth.bypass from bypassed routes', function () { + app.routes.get[1].callbacks[0].should.equal(publicAction); + }); + }); });