diff --git a/CHANGELOG.md b/CHANGELOG.md index ab86c24c..098eabd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## Next +### Improvements +* Valid token presence is verified on access token creation +* Valid tokenType presence is verified on `.revoke` calls + ### Maintainance * Remove dev lodash library * Update ava test runner to v3 diff --git a/lib/access-token.js b/lib/access-token.js index f2aa9b3a..2340bebc 100644 --- a/lib/access-token.js +++ b/lib/access-token.js @@ -1,6 +1,7 @@ 'use strict'; -const debug = require('debug')('access-token'); +const Hoek = require('@hapi/hoek'); +const debug = require('debug')('simple-oauth2:access-token'); const isDate = require('date-fns/isDate'); const parseISO = require('date-fns/parseISO'); const addSeconds = require('date-fns/addSeconds'); @@ -32,49 +33,65 @@ function parseToken(token) { return Object.assign({}, token, tokenProperties); } +const ACCESS_TOKEN_PROPERTY_NAME = 'access_token'; +const REFRESH_TOKEN_PROPERTY_NAME = 'refresh_token'; + module.exports = class AccessToken { static factory(config, client) { return (token) => new AccessToken(config, client, token); } constructor(config, client, token) { + Hoek.assert(config, 'Cannot create access token without client configuration'); + Hoek.assert(client, 'Cannot create access token without client instance'); + Hoek.assert(token, 'Cannot create access token without a token to parse'); + this.config = config; this.client = client; this.token = parseToken(token); } /** - * Check if the access token is expired or not + * Check if the current access token is definitely expired or not + * + * @returns {Boolean} */ expired() { return isAfter(new Date(), this.token.expires_at); } /** - * Refresh the access token + * Refreshes the current access token + * * @param {Object} params An optional argument for additional API request params. * @param {String|Array} params.scope A String or array of strings representing the application privileges + * @returns {Promise} */ async refresh(params = {}) { const refreshParams = Object.assign({}, params, { refresh_token: this.token.refresh_token, }); - const parameters = GrantParams.forGrant('refresh_token', refreshParams); + const parameters = GrantParams.forGrant(REFRESH_TOKEN_PROPERTY_NAME, refreshParams); const response = await this.client.request(this.config.auth.tokenPath, parameters.toObject()); return new AccessToken(this.config, this.client, response); } /** - * Revoke access or refresh token - * @param {String} tokenType A string containing the type of token to revoke. - * Should be either "access_token" or "refresh_token" + * Revokes either the access or refresh token depending on the {tokenType} value + * + * @param {String} tokenType A string containing the type of token to revoke (access_token or refresh_token) + * @returns {Promise} */ async revoke(tokenType) { - const token = tokenType === 'access_token' ? this.token.access_token : this.token.refresh_token; + Hoek.assert( + tokenType === ACCESS_TOKEN_PROPERTY_NAME || tokenType === REFRESH_TOKEN_PROPERTY_NAME, + `Invalid token type. Only ${ACCESS_TOKEN_PROPERTY_NAME} or ${REFRESH_TOKEN_PROPERTY_NAME} are valid values` + ); + const options = { - token, + token: this.token[tokenType], token_type_hint: tokenType, }; @@ -82,10 +99,11 @@ module.exports = class AccessToken { } /** - * Revoke both the existing access and refresh tokens + * Revokes the current access and refresh tokens + * @returns {Promise} */ async revokeAll() { - await this.revoke('access_token'); - await this.revoke('refresh_token'); + await this.revoke(ACCESS_TOKEN_PROPERTY_NAME); + await this.revoke(REFRESH_TOKEN_PROPERTY_NAME); } }; diff --git a/lib/client.js b/lib/client.js index 5adc1e5c..25244617 100644 --- a/lib/client.js +++ b/lib/client.js @@ -5,12 +5,14 @@ const Wreck = require('@hapi/wreck'); const debug = require('debug')('simple-oauth2:client'); const RequestOptions = require('./request-options'); +const defaultHttpHeaders = { + Accept: 'application/json', +}; + const defaultHttpOptions = { json: 'strict', redirects: 20, - headers: { - Accept: 'application/json', - }, + headers: defaultHttpHeaders, }; module.exports = class Client { @@ -32,8 +34,8 @@ module.exports = class Client { debug('Creating request to: (POST) %s', url); debug('Using request options: %j', options); - const result = await this.client.post(url, options); + const response = await this.client.post(url, options); - return result.payload; + return response.payload; } }; diff --git a/test/access_token.js b/test/access_token.js index 4f1b4c45..5ae5764e 100644 --- a/test/access_token.js +++ b/test/access_token.js @@ -20,6 +20,15 @@ const scopeOptions = { }, }; +test('@create => throws an error when no token payload is provided', (t) => { + const config = createModuleConfig(); + const oauth2 = oauth2Module.create(config); + + t.throws(() => oauth2.accessToken.create(), { + message: /Cannot create access token without a token to parse/, + }); +}); + test('@create => creates a new access token instance', (t) => { const config = createModuleConfig(); const oauth2 = oauth2Module.create(config); @@ -359,6 +368,18 @@ test.serial('@revoke => performs a token revoke with a custom revoke path', asyn scope.done(); }); +test.serial('@revoke => throws an error with an invalid tokenType option', async (t) => { + const config = createModuleConfig(); + const oauth2 = oauth2Module.create(config); + + const accessTokenResponse = chance.accessToken(); + const accessToken = oauth2.accessToken.create(accessTokenResponse); + + await t.throwsAsync(() => accessToken.revoke('invalid_value'), { + message: /Invalid token type. Only access_token or refresh_token are valid values/, + }); +}); + test.serial('@revokeAll => revokes both the access and refresh tokens', async (t) => { const config = createModuleConfig(); const oauth2 = oauth2Module.create(config);