From 3fc1f3e1099b55fe779e46b134f38dab16acccbb Mon Sep 17 00:00:00 2001 From: Visvk Date: Wed, 30 Mar 2016 17:11:26 +0200 Subject: [PATCH 1/4] revoke-handler: implementation --- lib/handlers/revoke-handler.js | 269 ++++++++ lib/server.js | 13 + .../handlers/revoke-handler_test.js | 640 ++++++++++++++++++ test/integration/server_test.js | 52 ++ test/unit/handlers/revoke-handler_test.js | 78 +++ test/unit/server_test.js | 20 + 6 files changed, 1072 insertions(+) create mode 100644 lib/handlers/revoke-handler.js create mode 100644 test/integration/handlers/revoke-handler_test.js create mode 100644 test/unit/handlers/revoke-handler_test.js diff --git a/lib/handlers/revoke-handler.js b/lib/handlers/revoke-handler.js new file mode 100644 index 000000000..2c425e2cb --- /dev/null +++ b/lib/handlers/revoke-handler.js @@ -0,0 +1,269 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var InvalidArgumentError = require('../errors/invalid-argument-error'); +var InvalidClientError = require('../errors/invalid-client-error'); +var InvalidRequestError = require('../errors/invalid-request-error'); +var OAuthError = require('../errors/oauth-error'); +var Promise = require('bluebird'); +var Request = require('../request'); +var Response = require('../response'); +var ServerError = require('../errors/server-error'); +var auth = require('basic-auth'); +var is = require('../validator/is'); + +/** + * Constructor. + */ + +function RevokeHandler(options) { + options = options || {}; + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`'); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`'); + } + + this.model = options.model; +} + +/** + * Revoke Handler. + */ + +RevokeHandler.prototype.handle = function(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); + } + + if (request.method !== 'POST') { + return Promise.reject(new InvalidRequestError('Invalid request: method must be POST')); + } + + if (!request.is('application/x-www-form-urlencoded')) { + return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')); + } + + return Promise.bind(this) + .then(function() { + return this.getClient(request, response); + }) + .then(function(client){ + return this.handleRevokeToken(request, client); + }) + .then(function(){ + /** + * All necessary information is conveyed in the response code. + * + * @see https://tools.ietf.org/html/rfc7009#section-2.1 + */ + return {}; + }) + .catch(function(e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + + this.updateErrorResponse(response, e); + + throw e; + }); +}; + +/** + * Handle revoke token + */ + +RevokeHandler.prototype.handleRevokeToken = function(request, client) { + return Promise.bind(this) + .then(function() { + return this.getTokenFromRequest(request); + }).then(function (token){ + return this.getRefreshToken(token, client); + }).tap(function (token) { + return this.revokeToken(token); + }); +}; + +/** + * Get the client from the model. + */ + +RevokeHandler.prototype.getClient = function(request, response) { + var credentials = this.getClientCredentials(request); + + if (!credentials.clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if (!credentials.clientSecret) { + throw new InvalidRequestError('Missing parameter: `client_secret`'); + } + + if (!is.vschar(credentials.clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + if (!is.vschar(credentials.clientSecret)) { + throw new InvalidRequestError('Invalid parameter: `client_secret`'); + } + + return Promise.try(this.model.getClient, [credentials.clientId, credentials.clientSecret]) + .then(function(client) { + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if (!client.grants) { + throw new ServerError('Server error: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError('Server error: `grants` must be an array'); + } + + return client; + }) + .catch(function(e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if ((e instanceof InvalidClientError) && request.get('authorization')) { + response.set('WWW-Authenticate', 'Basic realm="Service"'); + + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + }); +}; + +/** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + +RevokeHandler.prototype.getClientCredentials = function(request) { + var credentials = auth(request); + + if (credentials) { + return { clientId: credentials.name, clientSecret: credentials.pass }; + } + + if (request.body.client_id && request.body.client_secret) { + return { clientId: request.body.client_id, clientSecret: request.body.client_secret }; + } + + throw new InvalidClientError('Invalid client: cannot retrieve client credentials'); +}; + +/** + * Get the token from the body. + * + * @see https://tools.ietf.org/html/rfc7009#section-2.1 + */ + +RevokeHandler.prototype.getTokenFromRequest = function(request) { + var bodyToken = request.body.token; + + if (!bodyToken) { + throw new InvalidRequestError('Missing parameter: `token`'); + } + + return bodyToken; +}; + + +/** + * Get refresh token. + */ + +RevokeHandler.prototype.getRefreshToken = function(token, client) { + + return Promise.try(this.model.getRefreshToken, token) + .then(function(token) { + if (!token) { + throw new InvalidRequestError('Invalid request: refresh token is invalid'); + } + + if (!token.client) { + throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object'); + } + + if (!token.user) { + throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object'); + } + + if (token.client.id !== client.id) { + throw new InvalidRequestError('Invalid request: refresh token is invalid'); + } + + if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { + throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); + } + + if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { + throw new InvalidRequestError('Invalid request: refresh token has expired'); + } + + return token; + }); +}; + +/** + * Revoke the refresh token. + * + */ + +RevokeHandler.prototype.revokeToken = function(token) { + return Promise.try(this.model.revokeToken, token) + .then(function(status) { + if (!status) { + throw new InvalidRequestError('Invalid request: refresh token is invalid'); + } + + return token; + }); +}; + +/** + * Update response when an error is thrown. + */ + +RevokeHandler.prototype.updateErrorResponse = function(response, error) { + response.body = { + error: error.name, + error_description: error.message + }; + + response.status = error.code; +}; + +/** + * Export constructor. + */ + +module.exports = RevokeHandler; diff --git a/lib/server.js b/lib/server.js index 570bd2b62..3a4901c70 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,6 +9,7 @@ var AuthenticateHandler = require('./handlers/authenticate-handler'); var AuthorizeHandler = require('./handlers/authorize-handler'); var InvalidArgumentError = require('./errors/invalid-argument-error'); var TokenHandler = require('./handlers/token-handler'); +var RevokeHandler = require('./handlers/revoke-handler'); /** * Constructor. @@ -77,6 +78,18 @@ OAuth2Server.prototype.token = function(request, response, options, callback) { .nodeify(callback); }; +/** + * Revoke a token. + */ + +OAuth2Server.prototype.revoke = function(request, response, options, callback) { + options = _.assign(this.options, options); + + return new RevokeHandler(options) + .handle(request, response) + .nodeify(callback); +}; + /** * Export constructor. */ diff --git a/test/integration/handlers/revoke-handler_test.js b/test/integration/handlers/revoke-handler_test.js new file mode 100644 index 000000000..8dc59a510 --- /dev/null +++ b/test/integration/handlers/revoke-handler_test.js @@ -0,0 +1,640 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var AccessDeniedError = require('../../../lib/errors/access-denied-error'); +var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +var InvalidClientError = require('../../../lib/errors/invalid-client-error'); +var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +var Promise = require('bluebird'); +var Request = require('../../../lib/request'); +var Response = require('../../../lib/response'); +var ServerError = require('../../../lib/errors/server-error'); +var RevokeHandler = require('../../../lib/handlers/revoke-handler'); +var should = require('should'); +var util = require('util'); + +/** + * Test `RevokeHandler` integration. + */ + +describe('RevokeHandler integration', function() { + describe('constructor()', function() { + + it('should throw an error if `options.model` is missing', function() { + try { + new RevokeHandler({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', function() { + try { + new RevokeHandler({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: model does not implement `getClient()`'); + } + }); + + it('should set the `model`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + handler.model.should.equal(model); + }); + }); + + describe('handle()', function() { + it('should throw an error if `request` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + try { + handler.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: `request` must be an instance of Request'); + } + }); + + it('should throw an error if `response` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + try { + handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid argument: `response` must be an instance of Response'); + } + }); + + it('should throw an error if the method is not `POST`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: 'GET', query: {} }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: method must be POST'); + }); + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); + }); + }); + + it('should throw the error if an oauth error is thrown', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'hash' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: cannot retrieve client credentials'); + }); + }); + + it('should throw the error if an oauth error is thrown', function() { + var model = { + getClient: function() { return { grants: ['password'] }; }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', function() { + var model = { + getClient: function() { + throw new Error('Unhandled exception'); + }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash' + }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + method: 'POST', + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it('should update the response if an error is thrown', function() { + var model = { + getClient: function() { + throw new Error('Unhandled exception'); + }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo' + }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + method: 'POST', + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function() { + response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); + response.status.should.equal(503); + }); + }); + + it('should return an empty object if successful', function() { + var token = { refreshToken: 'hash', client: {}, user: {} }; + var client = { grants: ['password'] }; + var model = { + getClient: function() { return client; }, + revokeToken: function() { return token; }, + getRefreshToken: function() { return { refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; } + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash' + }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + method: 'POST', + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(function(data) { + data.should.eql({}); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', function() { + it('should throw an error if `clientId` is invalid', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); + + try { + handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `clientId` is invalid', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, query: {} }); + + try { + handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + } + }); + + it('should throw an error if `client` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should throw an error if `client.grants` is missing', function() { + var model = { + getClient: function() { return {}; }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: missing client `grants`'); + }); + }); + + it('should throw a 401 error if the client is invalid and the request contains an authorization header', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ + body: {}, + headers: { 'authorization': util.format('Basic %s', new Buffer('foo:bar').toString('base64')) }, + method: {}, + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.getClient(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal('Invalid client: client is invalid'); + + response.get('WWW-Authenticate').should.equal('Basic realm="Service"'); + }); + }); + + it('should return a client', function() { + var client = { id: 12345, grants: [] }; + var model = { + getClient: function() { return client; }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request) + .then(function(data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + + it('should support promises', function() { + var model = { + getClient: function() { return Promise.resolve({ grants: [] }); }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function() { + var model = { + getClient: function() { return { grants: [] }; }, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + }); + + describe('getClientCredentials()', function() { + it('should throw an error if `client_id` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, query: {} }); + + try { + handler.getClientCredentials(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: cannot retrieve client credentials'); + } + }); + + it('should throw an error if `client_secret` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, query: {} }); + + try { + handler.getClientCredentials(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: cannot retrieve client credentials'); + } + }); + + describe('with `client_id` and `client_secret` in the request header as basic auth', function() { + it('should return a client', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ + body: {}, + headers: { + 'authorization': util.format('Basic %s', new Buffer('foo:bar').toString('base64')) + }, + method: {}, + query: {} + }); + var credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request body', function() { + it('should return a client', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, query: {} }); + var credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + }); + + describe('handleRevokeToken()', function() { + it('should throw an error if `token` is missing', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + return handler.handleRevokeToken(request) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + }); + }); + + it('should return a token', function() { + var client = { id: 12345, grants: ['password'] }; + + var model = { + getClient: function() {}, + revokeToken: function() { return 'hash'; }, + getRefreshToken: function() { return { refreshToken: 'hash', client: { id: 12345 }, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; } + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'hash' }, headers: {}, method: {}, query: {} }); + + return handler.handleRevokeToken(request, client) + .then(function(data) { + data.refreshToken.should.equal('hash'); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', function() { + it('should throw an error if the `refreshToken` is invalid', function() { + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: refresh token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', function() { + var client = { id: 12345 }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() { return { client: { id: 9999}, user: {} }; } + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: refresh token is invalid'); + }); + }); + }); + + describe('revokeToken()', function() { + it('should throw an error if the `refreshToken` is invalid', function() { + var token = {}; + var model = { + getClient: function() {}, + revokeToken: function() { return false; }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.revokeToken(token) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: refresh token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', function() { + var token = {}; + var model = { + getClient: function() {}, + revokeToken: function() { return token; }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.revokeToken(token) + .then(function(data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('getTokenFromRequest()', function() { + it('should throw an error if `accessToken` is missing', function() { + + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); + + try { + handler.getTokenFromRequest(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `token`'); + } + }); + }); + + describe('updateErrorResponse()', function() { + it('should set the `body`', function() { + var error = new AccessDeniedError('Cannot request a revoke'); + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a revoke'); + }); + + it('should set the `status`', function() { + var error = new AccessDeniedError('Cannot request a revoke'); + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); +}); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 84761fb21..6cdfe3b15 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -234,4 +234,56 @@ describe('Server integration', function() { server.token(request, response, null, next); }); }); + + describe('revoke()', function() { + + it('should return a promise', function() { + var model = { + getClient: function() { + return { id: 1234, grants: ['password'] }; + }, + getRefreshToken: function() { + return { + client: { + id: 1234 + }, + user: {} + }; + }, + revokeToken: function() { + return true; + } + }; + var server = new Server({ model: model }); + var request = new Request({ body: { client_id: 1234, client_secret: 'secret', token: 'hash', token_type_hint: 'refresh_token' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + var handler = server.revoke(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + + it('should support callbacks', function(next) { + var model = { + getClient: function() { + return { id: 1234, grants: ['password'] }; + }, + getRefreshToken: function() { + return { + client: { + id: 1234 + }, + user: {} + }; + }, + revokeToken: function() { + return true; + } + }; + var server = new Server({ model: model }); + var request = new Request({ body: { client_id: 1234, client_secret: 'secret', token: 'hash', token_type_hint: 'refresh_token' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); + var response = new Response({ body: {}, headers: {} }); + + server.revoke(request, response, null, next); + }); + }); }); diff --git a/test/unit/handlers/revoke-handler_test.js b/test/unit/handlers/revoke-handler_test.js new file mode 100644 index 000000000..994c4403d --- /dev/null +++ b/test/unit/handlers/revoke-handler_test.js @@ -0,0 +1,78 @@ +'use strict'; + +/** + * Module dependencies. + */ + +var Request = require('../../../lib/request'); +var RevokeHandler = require('../../../lib/handlers/revoke-handler'); +var sinon = require('sinon'); +var should = require('should'); + +/** + * Test `RevokeHandler`. + */ + +describe('RevokeHandler', function() { + describe('getClient()', function() { + it('should call `model.getClient()`', function() { + var model = { + getClient: sinon.stub().returns({ grants: ['password'] }), + revokeToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + return handler.getClient(request) + .then(function() { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal('secret'); + }) + .catch(should.fail); + }); + }); + + describe('getRefreshToken()', function() { + it('should call `model.getRefreshToken()`', function() { + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }) + }; + var handler = new RevokeHandler({ model: model }); + var token = 'hash'; + var client = {}; + + return handler.getRefreshToken(token, client) + .then(function() { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('revokeToken()', function() { + it('should call `model.revokeToken()`', function() { + var model = { + getClient: function() {}, + revokeToken: sinon.stub().returns( true), + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var token = { refreshToken: 'hash'}; + + return handler.revokeToken(token) + .then(function() { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + }) + .catch(should.fail); + }); + }); +}); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index e7c343f0c..416f1116d 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -9,6 +9,7 @@ var AuthorizeHandler = require('../../lib/handlers/authorize-handler'); var Promise = require('bluebird'); var Server = require('../../lib/server'); var TokenHandler = require('../../lib/handlers/token-handler'); +var RevokeHandler = require('../../lib/handlers/revoke-handler'); var sinon = require('sinon'); /** @@ -87,4 +88,23 @@ describe('Server', function() { TokenHandler.prototype.handle.restore(); }); }); + + describe('revoke()', function() { + it('should call `handle`', function() { + var model = { + getClient: function() {}, + getRefreshToken: function() {}, + revokeToken: function() {} + }; + var server = new Server({ model: model }); + + sinon.stub(RevokeHandler.prototype, 'handle').returns(Promise.resolve()); + + server.revoke('foo', 'bar'); + + RevokeHandler.prototype.handle.callCount.should.equal(1); + RevokeHandler.prototype.handle.firstCall.args[0].should.equal('foo'); + RevokeHandler.prototype.handle.restore(); + }); + }); }); From 592c8099f811199151ea829e55aced2cdae4792f Mon Sep 17 00:00:00 2001 From: visvk Date: Thu, 18 Aug 2016 10:46:43 +0200 Subject: [PATCH 2/4] revoke-handler: revoke accessToken - revoke accessToken implementation - The token being revoked must belong to the requesting client - invalid tokens do not cause an error response --- lib/handlers/revoke-handler.js | 104 +++++++--- .../handlers/revoke-handler_test.js | 182 ++++++++++++------ test/integration/server_test.js | 6 + test/unit/handlers/revoke-handler_test.js | 33 +++- test/unit/server_test.js | 1 + 5 files changed, 240 insertions(+), 86 deletions(-) diff --git a/lib/handlers/revoke-handler.js b/lib/handlers/revoke-handler.js index 2c425e2cb..c66b84a22 100644 --- a/lib/handlers/revoke-handler.js +++ b/lib/handlers/revoke-handler.js @@ -6,6 +6,7 @@ var InvalidArgumentError = require('../errors/invalid-argument-error'); var InvalidClientError = require('../errors/invalid-client-error'); +var InvalidTokenError = require('../errors/invalid-token-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); @@ -34,6 +35,10 @@ function RevokeHandler(options) { throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`'); } + if (!options.model.getAccessToken) { + throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`'); + } + if (!options.model.revokeToken) { throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`'); } @@ -66,40 +71,57 @@ RevokeHandler.prototype.handle = function(request, response) { .then(function() { return this.getClient(request, response); }) - .then(function(client){ + .then(function(client) { return this.handleRevokeToken(request, client); }) - .then(function(){ + .catch(function(e) { + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } /** * All necessary information is conveyed in the response code. * - * @see https://tools.ietf.org/html/rfc7009#section-2.1 + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * @see https://tools.ietf.org/html/rfc7009#section-2.2 */ - return {}; - }) - .catch(function(e) { - if (!(e instanceof OAuthError)) { - e = new ServerError(e); + if (!(e instanceof InvalidTokenError)) { + this.updateErrorResponse(response, e); } - this.updateErrorResponse(response, e); - throw e; }); }; /** - * Handle revoke token + * Revoke a refresh or access token. + * + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" */ RevokeHandler.prototype.handleRevokeToken = function(request, client) { return Promise.bind(this) .then(function() { return this.getTokenFromRequest(request); - }).then(function (token){ - return this.getRefreshToken(token, client); - }).tap(function (token) { - return this.revokeToken(token); + }) + .then(function(token) { + return Promise.any([ + this.getAccessToken(token, client), + this.getRefreshToken(token, client) + ]) + .catch(Promise.AggregateError, function(err) { + err.forEach(function(e) { + throw e; + }); + }) + .bind(this) + .tap(function(token) { + return this.revokeToken(token); + }); }); }; @@ -196,17 +218,15 @@ RevokeHandler.prototype.getTokenFromRequest = function(request) { return bodyToken; }; - /** * Get refresh token. */ RevokeHandler.prototype.getRefreshToken = function(token, client) { - return Promise.try(this.model.getRefreshToken, token) .then(function(token) { if (!token) { - throw new InvalidRequestError('Invalid request: refresh token is invalid'); + throw new InvalidTokenError('Invalid token: refresh token is invalid'); } if (!token.client) { @@ -218,7 +238,7 @@ RevokeHandler.prototype.getRefreshToken = function(token, client) { } if (token.client.id !== client.id) { - throw new InvalidRequestError('Invalid request: refresh token is invalid'); + throw new InvalidTokenError('Invalid token: refresh token client is invalid'); } if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { @@ -226,7 +246,7 @@ RevokeHandler.prototype.getRefreshToken = function(token, client) { } if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { - throw new InvalidRequestError('Invalid request: refresh token has expired'); + throw new InvalidTokenError('Invalid token: refresh token has expired'); } return token; @@ -234,15 +254,51 @@ RevokeHandler.prototype.getRefreshToken = function(token, client) { }; /** - * Revoke the refresh token. + * Get the access token from the model. + */ + +RevokeHandler.prototype.getAccessToken = function(token, client) { + return Promise.try(this.model.getAccessToken, token) + .then(function(accessToken) { + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (!accessToken.client) { + throw new ServerError('Server error: `getAccessToken()` did not return a `client` object'); + } + + if (!accessToken.user) { + throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); + } + + if (accessToken.client.id !== client.id) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (accessToken.accessTokenExpiresAt && !(accessToken.accessTokenExpiresAt instanceof Date)) { + throw new ServerError('Server error: `expires` must be a Date instance'); + } + + if (accessToken.accessTokenExpiresAt && accessToken.accessTokenExpiresAt < new Date()) { + throw new InvalidTokenError('Invalid token: access token has expired.'); + } + + return accessToken; + }); +}; + +/** + * Revoke the token. * + * @see https://tools.ietf.org/html/rfc6749#section-6 */ RevokeHandler.prototype.revokeToken = function(token) { return Promise.try(this.model.revokeToken, token) - .then(function(status) { - if (!status) { - throw new InvalidRequestError('Invalid request: refresh token is invalid'); + .then(function(token) { + if (!token) { + throw new InvalidTokenError('Invalid token: token is invalid'); } return token; diff --git a/test/integration/handlers/revoke-handler_test.js b/test/integration/handlers/revoke-handler_test.js index 8dc59a510..053f21103 100644 --- a/test/integration/handlers/revoke-handler_test.js +++ b/test/integration/handlers/revoke-handler_test.js @@ -8,6 +8,7 @@ var AccessDeniedError = require('../../../lib/errors/access-denied-error'); var InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); var InvalidClientError = require('../../../lib/errors/invalid-client-error'); var InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +var InvalidTokenError = require('../../../lib/errors/invalid-token-error'); var Promise = require('bluebird'); var Request = require('../../../lib/request'); var Response = require('../../../lib/response'); @@ -49,7 +50,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); @@ -62,7 +64,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); @@ -80,7 +83,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -99,7 +103,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: {}, headers: {}, method: 'GET', query: {} }); @@ -117,7 +122,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: {}, headers: {}, method: 'POST', query: {} }); @@ -135,7 +141,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { token: 'hash' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); @@ -153,7 +160,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() { return { grants: ['password'] }; }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); @@ -173,7 +181,8 @@ describe('RevokeHandler integration', function() { throw new Error('Unhandled exception'); }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ @@ -203,7 +212,8 @@ describe('RevokeHandler integration', function() { throw new Error('Unhandled exception'); }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ @@ -228,13 +238,45 @@ describe('RevokeHandler integration', function() { }); }); + it('should not update the response if an invalid token error is thrown', function() { + var token = { refreshToken: 'hash', client: {}, user: {}, refreshTokenExpiresAt: new Date('2015-01-01') }; + var client = { grants: ['password'] }; + var model = { + getClient: function() { return client; }, + revokeToken: function() { return token; }, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + token: 'hash' + }, + headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, + method: 'POST', + query: {} + }); + var response = new Response({ body: {}, headers: {} }); + + return handler.handle(request, response) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + response.body.should.eql({}); + response.status.should.equal(200); + }); + }); + it('should return an empty object if successful', function() { - var token = { refreshToken: 'hash', client: {}, user: {} }; + var token = { refreshToken: 'hash', client: {}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; var client = { grants: ['password'] }; var model = { getClient: function() { return client; }, revokeToken: function() { return token; }, - getRefreshToken: function() { return { refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; } + getRefreshToken: function() { return token; }, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ @@ -251,7 +293,7 @@ describe('RevokeHandler integration', function() { return handler.handle(request, response) .then(function(data) { - data.should.eql({}); + should.exist(data); }) .catch(should.fail); }); @@ -262,7 +304,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); @@ -281,7 +324,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, query: {} }); @@ -300,7 +344,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -317,7 +362,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() { return {}; }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -334,7 +380,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ @@ -361,7 +408,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() { return client; }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -377,7 +425,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() { return Promise.resolve({ grants: [] }); }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -389,7 +438,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() { return { grants: [] }; }, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -403,7 +453,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, query: {} }); @@ -422,7 +473,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, query: {} }); @@ -442,7 +494,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ @@ -464,7 +517,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, query: {} }); @@ -480,7 +534,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -495,18 +550,38 @@ describe('RevokeHandler integration', function() { it('should return a token', function() { var client = { id: 12345, grants: ['password'] }; + var token = { accessToken: 'hash', client: { id: 12345 }, accessTokenExpiresAt: new Date(new Date() * 2), user: {} }; + var model = { + getClient: function() {}, + revokeToken: function() { return token; }, + getRefreshToken: function() {}, + getAccessToken: function() { return token; } + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'hash' }, headers: {}, method: {}, query: {} }); + + return handler.handleRevokeToken(request, client) + .then(function(data) { + should.exist(data); + }) + .catch(should.fail); + }); + it('should return a token', function() { + var client = { id: 12345, grants: ['password'] }; + var token = { refreshToken: 'hash', client: { id: 12345 }, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; var model = { getClient: function() {}, - revokeToken: function() { return 'hash'; }, - getRefreshToken: function() { return { refreshToken: 'hash', client: { id: 12345 }, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; } + revokeToken: function() { return token; }, + getRefreshToken: function() { return token; }, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { token: 'hash' }, headers: {}, method: {}, query: {} }); return handler.handleRevokeToken(request, client) .then(function(data) { - data.refreshToken.should.equal('hash'); + should.exist(data); }) .catch(should.fail); }); @@ -518,15 +593,16 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); return handler.getRefreshToken('hash', client) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: refresh token is invalid'); + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: refresh token is invalid'); }); }); @@ -535,52 +611,39 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() { return { client: { id: 9999}, user: {} }; } + getRefreshToken: function() { return { client: { id: 9999}, user: {} }; }, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); return handler.getRefreshToken('hash', client) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: refresh token is invalid'); + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: refresh token client is invalid'); }); }); }); describe('revokeToken()', function() { it('should throw an error if the `refreshToken` is invalid', function() { - var token = {}; + var token = 'hash'; + var client = {}; var model = { getClient: function() {}, revokeToken: function() { return false; }, - getRefreshToken: function() {} + getRefreshToken: function() { return { client: {}, user: {}};}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); - return handler.revokeToken(token) + return handler.revokeToken(token, client) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: refresh token is invalid'); + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: token is invalid'); }); }); - - it('should throw an error if the `client_id` does not match', function() { - var token = {}; - var model = { - getClient: function() {}, - revokeToken: function() { return token; }, - getRefreshToken: function() {} - }; - var handler = new RevokeHandler({ model: model }); - - return handler.revokeToken(token) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); }); describe('getTokenFromRequest()', function() { @@ -589,7 +652,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: {}, headers: {}, method: {}, query: {} }); @@ -611,7 +675,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var response = new Response({ body: {}, headers: {} }); @@ -627,7 +692,8 @@ describe('RevokeHandler integration', function() { var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 6cdfe3b15..4db76d2ba 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -250,6 +250,9 @@ describe('Server integration', function() { user: {} }; }, + getAccessToken: function() { + return null; + }, revokeToken: function() { return true; } @@ -275,6 +278,9 @@ describe('Server integration', function() { user: {} }; }, + getAccessToken: function() { + return null; + }, revokeToken: function() { return true; } diff --git a/test/unit/handlers/revoke-handler_test.js b/test/unit/handlers/revoke-handler_test.js index 994c4403d..9cc56015d 100644 --- a/test/unit/handlers/revoke-handler_test.js +++ b/test/unit/handlers/revoke-handler_test.js @@ -14,12 +14,36 @@ var should = require('should'); */ describe('RevokeHandler', function() { + describe('handleRevokeToken()', function() { + it('should call `model.getAccessToken()` and `model.getRefreshToken()`', function() { + var model = { + getClient: function() {}, + revokeToken: sinon.stub().returns( true), + getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }), + getAccessToken: sinon.stub().returns( false) + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { token: 'foo' }, headers: {}, method: {}, query: {} }); + var client = {}; + + return handler.handleRevokeToken(request, client) + .then(function() { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args[0].should.equal('foo'); + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args[0].should.equal('foo'); + }) + .catch(should.fail); + }); + }); + describe('getClient()', function() { it('should call `model.getClient()`', function() { var model = { getClient: sinon.stub().returns({ grants: ['password'] }), revokeToken: function() {}, - getRefreshToken: function() {} + getRefreshToken: function() {}, + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); @@ -40,6 +64,7 @@ describe('RevokeHandler', function() { var model = { getClient: function() {}, revokeToken: function() {}, + getAccessToken: function() {}, getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }) }; var handler = new RevokeHandler({ model: model }); @@ -61,16 +86,16 @@ describe('RevokeHandler', function() { var model = { getClient: function() {}, revokeToken: sinon.stub().returns( true), - getRefreshToken: function() {} + getRefreshToken: sinon.stub().returns({ refreshToken: 'hash', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }), + getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); - var token = { refreshToken: 'hash'}; + var token = 'hash'; return handler.revokeToken(token) .then(function() { model.revokeToken.callCount.should.equal(1); model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); }) .catch(should.fail); }); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index 416f1116d..21083bef6 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -94,6 +94,7 @@ describe('Server', function() { var model = { getClient: function() {}, getRefreshToken: function() {}, + getAccessToken: function() {}, revokeToken: function() {} }; var server = new Server({ model: model }); From 267769376d56c517707f366d40e3e4185f2985b2 Mon Sep 17 00:00:00 2001 From: viktor sincak Date: Tue, 7 Aug 2018 14:34:17 +0200 Subject: [PATCH 3/4] Rebase revoke-handler to oauthjs:dev --- lib/handlers/revoke-handler.js | 11 +- .../handlers/revoke-handler_test.js | 160 +++++++++++++++++- 2 files changed, 163 insertions(+), 8 deletions(-) diff --git a/lib/handlers/revoke-handler.js b/lib/handlers/revoke-handler.js index c66b84a22..8ee09802d 100644 --- a/lib/handlers/revoke-handler.js +++ b/lib/handlers/revoke-handler.js @@ -10,6 +10,7 @@ var InvalidTokenError = require('../errors/invalid-token-error'); var InvalidRequestError = require('../errors/invalid-request-error'); var OAuthError = require('../errors/oauth-error'); var Promise = require('bluebird'); +var promisify = require('promisify-any'); var Request = require('../request'); var Response = require('../response'); var ServerError = require('../errors/server-error'); @@ -148,7 +149,7 @@ RevokeHandler.prototype.getClient = function(request, response) { throw new InvalidRequestError('Invalid parameter: `client_secret`'); } - return Promise.try(this.model.getClient, [credentials.clientId, credentials.clientSecret]) + return Promise.try(promisify(this.model.getClient, 2), [credentials.clientId, credentials.clientSecret]) .then(function(client) { if (!client) { throw new InvalidClientError('Invalid client: client is invalid'); @@ -223,7 +224,7 @@ RevokeHandler.prototype.getTokenFromRequest = function(request) { */ RevokeHandler.prototype.getRefreshToken = function(token, client) { - return Promise.try(this.model.getRefreshToken, token) + return Promise.try(promisify(this.model.getRefreshToken, 1), token) .then(function(token) { if (!token) { throw new InvalidTokenError('Invalid token: refresh token is invalid'); @@ -258,7 +259,7 @@ RevokeHandler.prototype.getRefreshToken = function(token, client) { */ RevokeHandler.prototype.getAccessToken = function(token, client) { - return Promise.try(this.model.getAccessToken, token) + return Promise.try(promisify(this.model.getAccessToken, 1), token) .then(function(accessToken) { if (!accessToken) { throw new InvalidTokenError('Invalid token: access token is invalid'); @@ -273,7 +274,7 @@ RevokeHandler.prototype.getAccessToken = function(token, client) { } if (accessToken.client.id !== client.id) { - throw new InvalidTokenError('Invalid token: access token is invalid'); + throw new InvalidTokenError('Invalid token: access token client is invalid'); } if (accessToken.accessTokenExpiresAt && !(accessToken.accessTokenExpiresAt instanceof Date)) { @@ -295,7 +296,7 @@ RevokeHandler.prototype.getAccessToken = function(token, client) { */ RevokeHandler.prototype.revokeToken = function(token) { - return Promise.try(this.model.revokeToken, token) + return Promise.try(promisify(this.model.revokeToken, 1), token) .then(function(token) { if (!token) { throw new InvalidTokenError('Invalid token: token is invalid'); diff --git a/test/integration/handlers/revoke-handler_test.js b/test/integration/handlers/revoke-handler_test.js index 053f21103..545aae35d 100644 --- a/test/integration/handlers/revoke-handler_test.js +++ b/test/integration/handlers/revoke-handler_test.js @@ -234,7 +234,7 @@ describe('RevokeHandler integration', function() { .then(should.fail) .catch(function() { response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); - response.status.should.equal(503); + response.status.should.equal(500); }); }); @@ -434,6 +434,21 @@ describe('RevokeHandler integration', function() { handler.getClient(request).should.be.an.instanceOf(Promise); }); + it('should support callbacks', function() { + var model = { + getClient: function(clientId, clientSecret, callback) { + callback(null, { grants: [] }); + }, + revokeToken: function() {}, + getRefreshToken: function() {}, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + var request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + it('should support non-promises', function() { var model = { getClient: function() { return { grants: [] }; }, @@ -607,11 +622,12 @@ describe('RevokeHandler integration', function() { }); it('should throw an error if the `client_id` does not match', function() { - var client = { id: 12345 }; + var client = { id: 'foo' }; + var token = { refreshToken: 'hash', client: { id: 'baz'}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; var model = { getClient: function() {}, revokeToken: function() {}, - getRefreshToken: function() { return { client: { id: 9999}, user: {} }; }, + getRefreshToken: function() { return token; }, getAccessToken: function() {} }; var handler = new RevokeHandler({ model: model }); @@ -623,6 +639,121 @@ describe('RevokeHandler integration', function() { e.message.should.equal('Invalid token: refresh token client is invalid'); }); }); + + it('should return a token', function() { + var client = { id: 'foo' }; + var token = { refreshToken: 'hash', client: { id: 'foo'}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function() { return token; }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + + it('should support callbacks', function() { + var client = { id: 'foo' }; + var token = { refreshToken: 'hash', client: { id: 'foo'}, user: {}, refreshTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getRefreshToken: function(refreshToken, callback) { + callback(null, token); + }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getRefreshToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + }); + + describe('getAccessToken()', function() { + it('should throw an error if the `accessToken` is invalid', function() { + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() {}, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token is invalid'); + }); + }); + + it('should throw an error if the `client_id` does not match', function() { + var client = { id: 'foo' }; + var token = { accessToken: 'hash', client: { id: 'baz'}, user: {}, accessTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() { return token; }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token client is invalid'); + }); + }); + + it('should return a token', function() { + var client = { id: 'foo' }; + var token = { accessToken: 'hash', client: { id: 'foo'}, user: {}, accessTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function() { return token; }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); + + it('should support callbacks', function() { + var client = { id: 'foo' }; + var token = { accessToken: 'hash', client: { id: 'foo'}, user: {}, accessTokenExpiresAt: new Date(new Date() * 2) }; + var model = { + getClient: function() {}, + revokeToken: function() {}, + getAccessToken: function(accessToken, callback) { + callback(null, token); + }, + getRefreshToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.getAccessToken('hash', client) + .then(function(token) { + should.exist(token); + }) + .catch(should.fail); + }); }); describe('revokeToken()', function() { @@ -644,6 +775,29 @@ describe('RevokeHandler integration', function() { e.message.should.equal('Invalid token: token is invalid'); }); }); + + it('should support callbacks', function() { + var token = {}; + var client = {}; + var model = { + getClient: function() {}, + revokeToken: function(tokenObject, callback) { + callback(null, null); + }, + getRefreshToken: function(refreshToken, callback) { + callback(null, { client: {}, user: {}}); + }, + getAccessToken: function() {} + }; + var handler = new RevokeHandler({ model: model }); + + return handler.revokeToken(token, client) + .then(should.fail) + .catch(function(e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: token is invalid'); + }); + }); }); describe('getTokenFromRequest()', function() { From 6a61aa57a7ba5e0f88e4a441a1afd68ab1bec336 Mon Sep 17 00:00:00 2001 From: visvk Date: Mon, 22 Aug 2016 18:07:57 +0200 Subject: [PATCH 4/4] revoke-handler: throw InvalidClientError if client_id does not match with token.client.id --- lib/handlers/revoke-handler.js | 4 ++-- test/integration/handlers/revoke-handler_test.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/handlers/revoke-handler.js b/lib/handlers/revoke-handler.js index 8ee09802d..db02ababa 100644 --- a/lib/handlers/revoke-handler.js +++ b/lib/handlers/revoke-handler.js @@ -239,7 +239,7 @@ RevokeHandler.prototype.getRefreshToken = function(token, client) { } if (token.client.id !== client.id) { - throw new InvalidTokenError('Invalid token: refresh token client is invalid'); + throw new InvalidClientError('Invalid client: client is invalid'); } if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { @@ -274,7 +274,7 @@ RevokeHandler.prototype.getAccessToken = function(token, client) { } if (accessToken.client.id !== client.id) { - throw new InvalidTokenError('Invalid token: access token client is invalid'); + throw new InvalidClientError('Invalid client: client is invalid'); } if (accessToken.accessTokenExpiresAt && !(accessToken.accessTokenExpiresAt instanceof Date)) { diff --git a/test/integration/handlers/revoke-handler_test.js b/test/integration/handlers/revoke-handler_test.js index 545aae35d..8327f72ff 100644 --- a/test/integration/handlers/revoke-handler_test.js +++ b/test/integration/handlers/revoke-handler_test.js @@ -635,8 +635,8 @@ describe('RevokeHandler integration', function() { return handler.getRefreshToken('hash', client) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: refresh token client is invalid'); + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); }); }); @@ -712,8 +712,8 @@ describe('RevokeHandler integration', function() { return handler.getAccessToken('hash', client) .then(should.fail) .catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token client is invalid'); + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); }); });