Source: sessions/app-auth-session.ts

  1. /**
  2. * @fileoverview App Auth Box API Session.
  3. */
  4. // ------------------------------------------------------------------------------
  5. // Requirements
  6. // ------------------------------------------------------------------------------
  7. import assert from 'assert';
  8. import { Promise } from 'bluebird';
  9. import errors from '../util/errors';
  10. // ------------------------------------------------------------------------------
  11. // Typedefs
  12. // ------------------------------------------------------------------------------
  13. type Config = any /* FIXME */;
  14. type TokenManager = any /* FIXME */;
  15. type TokenStore = any /* FIXME */;
  16. type TokenInfo = any /* FIXME */;
  17. type TokenRequestOptions = any /* FIXME */;
  18. // ------------------------------------------------------------------------------
  19. // Private
  20. // ------------------------------------------------------------------------------
  21. /**
  22. * Validate that an object is a valid TokenStore object
  23. *
  24. * @param {Object} obj the object to validate
  25. * @returns {boolean} returns true if the passed in object is a valid TokenStore object that
  26. * has all the expected properties. false otherwise.
  27. * @private
  28. */
  29. function isObjectValidTokenStore(obj: Record<string, any>) {
  30. return Boolean(obj && obj.read && obj.write && obj.clear);
  31. }
  32. // ------------------------------------------------------------------------------
  33. // Public
  34. // ------------------------------------------------------------------------------
  35. /**
  36. * App Auth Box API Session.
  37. *
  38. * The App Auth API Session holds an accessToken for an app user or enterprise,
  39. * which it returns to the client so that it may make calls on behalf of
  40. * these entities.
  41. *
  42. * These access tokens will be refreshed in the background if a request is made within the
  43. * "stale buffer" (defaults to 10 minutes before the token is set to expire).
  44. * If the token is also expired, all incoming requests will be held until a fresh token
  45. * is retrieved.
  46. *
  47. * @param {string} type The type of the entity to authenticate the app auth session as, "user" or "enterprise"
  48. * @param {string} id The Box ID of the entity to authenticate as
  49. * @param {Config} config The SDK configuration options
  50. * @param {TokenManager} tokenManager The TokenManager
  51. * @param {TokenStore} [tokenStore] The token store instance to use for caching token info
  52. * @constructor
  53. */
  54. class AppAuthSession {
  55. _type: string;
  56. _id: string;
  57. _config: Config;
  58. _tokenManager: TokenManager;
  59. _tokenStore: TokenStore | null;
  60. _tokenInfo: TokenInfo;
  61. _refreshPromise: Promise<any> | null;
  62. constructor(
  63. type: string,
  64. id: string,
  65. config: Config,
  66. tokenManager: TokenManager,
  67. tokenStore?: TokenStore
  68. ) {
  69. this._type = type;
  70. this._id = id;
  71. this._config = config;
  72. this._tokenManager = tokenManager;
  73. // If tokenStore was provided, set the persistent data & current store operations
  74. if (tokenStore) {
  75. assert(
  76. isObjectValidTokenStore(tokenStore),
  77. 'Token store provided is improperly formatted. Methods required: read(), write(), clear().'
  78. );
  79. this._tokenStore = Promise.promisifyAll(tokenStore);
  80. }
  81. // The TokenInfo object for this app auth session
  82. this._tokenInfo = null;
  83. // Indicates if tokens are currently being refreshed
  84. this._refreshPromise = null;
  85. }
  86. /**
  87. * Initiate a refresh of the app auth access tokens. New tokens should be passed
  88. * to the caller, and then cached for later use.
  89. *
  90. * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant
  91. * @returns {Promise<string>} Promise resolving to the access token
  92. * @private
  93. */
  94. _refreshAppAuthAccessToken(options?: TokenRequestOptions) {
  95. // If tokens aren't already being refreshed, start the refresh
  96. if (!this._refreshPromise) {
  97. this._refreshPromise = this._tokenManager
  98. .getTokensJWTGrant(this._type, this._id, options)
  99. .then((tokenInfo: TokenInfo) => {
  100. // Set new token info and propagate the new access token
  101. this._tokenInfo = tokenInfo;
  102. if (this._tokenStore) {
  103. return this._tokenStore
  104. .writeAsync(tokenInfo)
  105. .then(() => tokenInfo.accessToken);
  106. }
  107. return tokenInfo.accessToken;
  108. })
  109. .finally(() => {
  110. // Refresh complete, clear promise
  111. this._refreshPromise = null;
  112. });
  113. }
  114. return this._refreshPromise;
  115. }
  116. /**
  117. * Produces a valid, app auth access token.
  118. * Performs a refresh before returning if the current token is expired. If the current
  119. * token is considered stale but still valid, return the current token but initiate a
  120. * new refresh in the background.
  121. *
  122. * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant
  123. * @returns {Promise<string>} Promise resolving to the access token
  124. */
  125. getAccessToken(options?: TokenRequestOptions) {
  126. var expirationBuffer = this._config.expiredBufferMS;
  127. // If we're initializing the client and have a token store, try reading from it
  128. if (!this._tokenInfo && this._tokenStore) {
  129. return this._tokenStore.readAsync().then((tokenInfo: TokenInfo) => {
  130. if (
  131. !this._tokenManager.isAccessTokenValid(tokenInfo, expirationBuffer)
  132. ) {
  133. // Token store contains expired tokens, refresh
  134. return this._refreshAppAuthAccessToken(options);
  135. }
  136. this._tokenInfo = tokenInfo;
  137. return tokenInfo.accessToken;
  138. });
  139. }
  140. // If the current token is not fresh, get a new token. All incoming
  141. // requests will be held until a fresh token is retrieved.
  142. if (
  143. !this._tokenInfo ||
  144. !this._tokenManager.isAccessTokenValid(this._tokenInfo, expirationBuffer)
  145. ) {
  146. return this._refreshAppAuthAccessToken(options);
  147. }
  148. // Your token is not currently stale! Return the current access token.
  149. return Promise.resolve(this._tokenInfo.accessToken);
  150. }
  151. /**
  152. * Revokes the app auth token used by this session, and clears the saved tokenInfo.
  153. *
  154. * @param {TokenRequestOptions} [options]- Sets optional behavior for the token grant
  155. * @returns {Promise} Promise resolving if the revoke succeeds
  156. */
  157. revokeTokens(options: TokenRequestOptions) {
  158. // The current app auth token is revoked (but a new one will be created automatically as needed).
  159. var tokenInfo = this._tokenInfo || {},
  160. accessToken = tokenInfo.accessToken;
  161. this._tokenInfo = null;
  162. return this._tokenManager.revokeTokens(accessToken, options);
  163. }
  164. /**
  165. * Exchange the client access token for one with lower scope
  166. * @param {string|string[]} scopes The scope(s) requested for the new token
  167. * @param {string} [resource] The absolute URL of an API resource to scope the new token to
  168. * @param {Object} [options] - Optional parameters
  169. * @param {TokenRequestOptions} [options.tokenRequestOptions] - Sets optional behavior for the token grant
  170. * @param {ActorParams} [options.actor] - Optional actor parameters for creating annotator tokens
  171. * @returns {Promise<TokenInfo>} Promise resolving to the new token info
  172. */
  173. exchangeToken(
  174. scopes: string | string[],
  175. resource?: string,
  176. options?: {
  177. tokenRequestOptions?: TokenRequestOptions;
  178. actor?: any /* FIXME */;
  179. }
  180. ) {
  181. return this.getAccessToken(options).then((accessToken: string) =>
  182. this._tokenManager.exchangeToken(accessToken, scopes, resource, options)
  183. );
  184. }
  185. /**
  186. * Handle an an "Expired Tokens" Error. If our tokens are expired, we need to clear the token
  187. * store (if present) before continuing.
  188. *
  189. * @param {Errors~ExpiredTokensError} err An "expired tokens" error including information
  190. * about the request/response.
  191. * @returns {Promise<Error>} Promise resolving to an error. This will
  192. * usually be the original response error, but could an error from trying to access the
  193. * token store as well.
  194. */
  195. handleExpiredTokensError(err: any /* FIXME */) {
  196. if (!this._tokenStore) {
  197. return Promise.resolve(err);
  198. }
  199. // If a token store is available, clear the store and throw either error
  200. // eslint-disable-next-line promise/no-promise-in-callback
  201. return this._tokenStore
  202. .clearAsync()
  203. .catch((e: any) => errors.unwrapAndThrow(e))
  204. .then(() => {
  205. throw err;
  206. });
  207. }
  208. }
  209. /**
  210. * @module box-node-sdk/lib/sessions/app-auth-session
  211. * @see {@Link AppAuthSession}
  212. */
  213. export = AppAuthSession;