diff --git a/docs/auth_TokensManager.js.html b/docs/auth_TokensManager.js.html index a78ed591d..9a4020ec5 100644 --- a/docs/auth_TokensManager.js.html +++ b/docs/auth_TokensManager.js.html @@ -50,10 +50,11 @@

auth/TokensManager.js

* @constructor * @memberOf module:auth * - * @param {Object} options Manager options. - * @param {String} options.baseUrl The auth0 account URL. - * @param {String} [options.headers] Default request headers. - * @param {String} [options.clientId] Default client ID. + * @param {Object} options Manager options. + * @param {String} options.baseUrl The auth0 account URL. + * @param {String} [options.headers] Default request headers. + * @param {String} [options.clientId] Default client ID. + * @param {String} [options.clientSecret] Default client Secret. */ var TokensManager = function(options) { if (typeof options !== 'object') { @@ -67,6 +68,7 @@

auth/TokensManager.js

this.baseUrl = options.baseUrl; this.headers = options.headers || {}; this.clientId = options.clientId || ''; + this.clientSecret = options.clientSecret || ''; }; /** @@ -213,6 +215,86 @@

auth/TokensManager.js

return promise; }; +/** + * Proactively revoke an issued refresh token. + * + * @method + * @memberOf module:auth.TokensManager.prototype + * + * @example <caption> + * Given an existing refresh token, this endpoint will revoke it in order + * to prevent unauthorized silently user authentication tokens refresh. + * Find more information in the <a href="https://auth0.com/docs/api/authentication#revoke-refresh-token">API Docs</a>. + * </caption> + * + * * var data = { + * token: '{REFRESH_TOKEN}' + * }; + * + * auth0.tokens.revokeRefreshToken(data, function (err, _) { + * if (err) { + * // Handle error. + * } + * + * // Do stuff. + * }); + * + * @param {Object} data Token data object. + * @param {String} data.token User refresh token. + * @param {String} [data.client_id] Target client ID. + * @param {String} [data.client_secret] Target client secret. + * @param {Function} [cb] Callback function. + * + * @return {Promise|undefined} + */ +TokensManager.prototype.revokeRefreshToken = function(data, cb) { + if (!data) { + throw new ArgumentError('Missing token data object'); + } + + var hasToken = typeof data.token === 'string' && data.token.trim().length !== 0; + + if (!hasToken) { + throw new ArgumentError('token property is required'); + } + + var hasClientId = + (data.client_id && typeof data.client_id === 'string' && data.client_id.trim().length !== 0) || + this.clientId !== ''; + + if (!hasClientId) { + throw new ArgumentError( + 'Neither token data client_id property or constructor clientId property has been set' + ); + } + + var body = extend( + { + client_id: this.clientId, + client_secret: this.clientSecret + }, + data + ); + + var headers = this.headers; + + // Perform the request. + var promise = axios({ + method: 'POST', + url: this.baseUrl + '/oauth/revoke', + data: body, + headers: headers + }).then(({ data }) => data); + + // Use callback if given. + if (cb instanceof Function) { + promise.then(cb.bind(null, null)).catch(cb); + return; + } + + return promise; +}; + module.exports = TokensManager; @@ -226,7 +308,7 @@

auth/TokensManager.js


diff --git a/src/auth/TokensManager.js b/src/auth/TokensManager.js index 04b08b10e..112f18b62 100644 --- a/src/auth/TokensManager.js +++ b/src/auth/TokensManager.js @@ -9,10 +9,11 @@ var ArgumentError = require('rest-facade').ArgumentError; * @constructor * @memberOf module:auth * - * @param {Object} options Manager options. - * @param {String} options.baseUrl The auth0 account URL. - * @param {String} [options.headers] Default request headers. - * @param {String} [options.clientId] Default client ID. + * @param {Object} options Manager options. + * @param {String} options.baseUrl The auth0 account URL. + * @param {String} [options.headers] Default request headers. + * @param {String} [options.clientId] Default client ID. + * @param {String} [options.clientSecret] Default client Secret. */ var TokensManager = function(options) { if (typeof options !== 'object') { @@ -26,6 +27,7 @@ var TokensManager = function(options) { this.baseUrl = options.baseUrl; this.headers = options.headers || {}; this.clientId = options.clientId || ''; + this.clientSecret = options.clientSecret || ''; }; /** @@ -172,4 +174,84 @@ TokensManager.prototype.getDelegationToken = function(data, cb) { return promise; }; +/** + * Proactively revoke an issued refresh token. + * + * @method + * @memberOf module:auth.TokensManager.prototype + * + * @example + * Given an existing refresh token, this endpoint will revoke it in order + * to prevent unauthorized silently user authentication tokens refresh. + * Find more information in the API Docs. + * + * + * * var data = { + * token: '{REFRESH_TOKEN}' + * }; + * + * auth0.tokens.revokeRefreshToken(data, function (err, _) { + * if (err) { + * // Handle error. + * } + * + * // Do stuff. + * }); + * + * @param {Object} data Token data object. + * @param {String} data.token User refresh token. + * @param {String} [data.client_id] Target client ID. + * @param {String} [data.client_secret] Target client secret. + * @param {Function} [cb] Callback function. + * + * @return {Promise|undefined} + */ +TokensManager.prototype.revokeRefreshToken = function(data, cb) { + if (!data) { + throw new ArgumentError('Missing token data object'); + } + + var hasToken = typeof data.token === 'string' && data.token.trim().length !== 0; + + if (!hasToken) { + throw new ArgumentError('token property is required'); + } + + var hasClientId = + (data.client_id && typeof data.client_id === 'string' && data.client_id.trim().length !== 0) || + this.clientId !== ''; + + if (!hasClientId) { + throw new ArgumentError( + 'Neither token data client_id property or constructor clientId property has been set' + ); + } + + var body = extend( + { + client_id: this.clientId, + client_secret: this.clientSecret + }, + data + ); + + var headers = this.headers; + + // Perform the request. + var promise = axios({ + method: 'POST', + url: this.baseUrl + '/oauth/revoke', + data: body, + headers: headers + }).then(({ data }) => data); + + // Use callback if given. + if (cb instanceof Function) { + promise.then(cb.bind(null, null)).catch(cb); + return; + } + + return promise; +}; + module.exports = TokensManager; diff --git a/test/auth/tokens-manager.tests.js b/test/auth/tokens-manager.tests.js index a9a4c9c9b..f77838a51 100644 --- a/test/auth/tokens-manager.tests.js +++ b/test/auth/tokens-manager.tests.js @@ -14,7 +14,8 @@ describe('TokensManager', function() { 'Content-Type': 'application/json', 'Test-Header': 'TEST' }, - clientId: 'CLIENT_ID' + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET' }; afterEach(function() { @@ -34,7 +35,7 @@ describe('TokensManager', function() { }); describe('instance', function() { - var methods = ['getInfo', 'getDelegationToken']; + var methods = ['getInfo', 'getDelegationToken', 'revokeRefreshToken']; var manager = new TokensManager(validOptions); methods.forEach(function(methodName) { @@ -327,4 +328,203 @@ describe('TokensManager', function() { }); }); }); + + describe('#revokeRefreshToken', function() { + var path = '/oauth/revoke'; + var manager = new TokensManager(validOptions); + + beforeEach(function() { + this.request = nock(BASE_URL) + .post(path) + .reply(200); + }); + + it('should require a token data object', function() { + var revokeRefreshToken = manager.revokeRefreshToken.bind(manager); + + expect(revokeRefreshToken).to.throw(ArgumentError, 'Missing token data object'); + }); + + it('should require a token property in the token data object', function() { + var data = {}; + var revokeRefreshToken = manager.revokeRefreshToken.bind(manager, data); + + expect(revokeRefreshToken).to.throw(ArgumentError, 'token property is required'); + }); + + it('should require at least a target client ID', function() { + var manager = new TokensManager({ + baseUrl: BASE_URL, + headers: { + 'Content-Type': 'application/json', + 'Test-Header': 'TEST' + } + }); + + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + + var revokeRefreshToken = manager.revokeRefreshToken.bind(manager, data); + + expect(revokeRefreshToken).to.throw( + ArgumentError, + 'Neither token data client_id property or constructor clientId property has been set' + ); + }); + + it('should accept a callback', function(done) { + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + + manager.revokeRefreshToken(data, done.bind(null, null)); + }); + + it('should return a promise when no callback is given', function() { + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + var returnValue = manager.revokeRefreshToken(data); + expect(utilTypes.isPromise(returnValue)).ok; + }); + + it('should not return a promise when a callback is given', function() { + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + var returnValue = manager.revokeRefreshToken(data, function() {}); + + expect(returnValue).to.equal(undefined); + }); + + it('should perform a POST request to ' + path, function() {}); + + it('should include the data in the body of the request', function(done) { + nock.cleanAll(); + + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + + var request = nock(BASE_URL) + .post(path, function(body) { + for (var property in data) { + if (body[property] !== data[property]) { + return false; + } + } + + return true; + }) + .reply(); + + manager.revokeRefreshToken(data).then(function() { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + + it('should use the TokensManager instance client ID if none specified', function(done) { + nock.cleanAll(); + + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + + var request = nock(BASE_URL) + .post(path, function(body) { + return body.client_id === validOptions.clientId; + }) + .reply(); + + manager.revokeRefreshToken(data).then(function() { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + + it('should let the user override the default client ID', function(done) { + nock.cleanAll(); + + var data = { + token: 'TEST_REFRESH_TOKEN', + client_id: 'OVERRIDEN_CLIENT_ID' + }; + + var request = nock(BASE_URL) + .post(path, function(body) { + return body.client_id === data.client_id; + }) + .reply(); + + manager.revokeRefreshToken(data).then(function() { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + + it('should use the TokensManager instance client secret if none specified', function(done) { + nock.cleanAll(); + + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + + var request = nock(BASE_URL) + .post(path, function(body) { + return body.client_secret === validOptions.clientSecret; + }) + .reply(); + + manager.revokeRefreshToken(data).then(function() { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + + it('should let the user override the default client secret', function(done) { + nock.cleanAll(); + + var data = { + token: 'TEST_REFRESH_TOKEN', + client_secret: 'OVERRIDEN_CLIENT_SECRET' + }; + + var request = nock(BASE_URL) + .post(path, function(body) { + return body.client_secret === data.client_secret; + }) + .reply(); + + manager.revokeRefreshToken(data).then(function() { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + + it('should include the headers specified in the instance options', function(done) { + nock.cleanAll(); + + var data = { + token: 'TEST_REFRESH_TOKEN' + }; + + var request = nock(BASE_URL) + .post(path) + .matchHeader('Test-Header', validOptions.headers['Test-Header']) + .reply(200); + + manager.revokeRefreshToken(data).then(function() { + expect(request.isDone()).to.be.true; + + done(); + }); + }); + }); });