sso_token_provider.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. var AWS = require('../core');
  2. var crypto = require('crypto');
  3. var fs = require('fs');
  4. var path = require('path');
  5. var iniLoader = AWS.util.iniLoader;
  6. // Tracking refresh attempt to ensure refresh is not attempted more than once every 30 seconds.
  7. var lastRefreshAttemptTime = 0;
  8. /**
  9. * Throws error is key is not present in token object.
  10. *
  11. * @param token [Object] Object to be validated.
  12. * @param key [String] The key to be validated on the object.
  13. */
  14. var validateTokenKey = function validateTokenKey(token, key) {
  15. if (!token[key]) {
  16. throw AWS.util.error(
  17. new Error('Key "' + key + '" not present in SSO Token'),
  18. { code: 'SSOTokenProviderFailure' }
  19. );
  20. }
  21. };
  22. /**
  23. * Calls callback function with or without error based on provided times in case
  24. * of unsuccessful refresh.
  25. *
  26. * @param currentTime [number] current time in milliseconds since ECMAScript epoch.
  27. * @param tokenExpireTime [number] token expire time in milliseconds since ECMAScript epoch.
  28. * @param callback [Function] Callback to call in case of error.
  29. */
  30. var refreshUnsuccessful = function refreshUnsuccessful(
  31. currentTime,
  32. tokenExpireTime,
  33. callback
  34. ) {
  35. if (tokenExpireTime > currentTime) {
  36. // Cached token is still valid, return.
  37. callback(null);
  38. } else {
  39. // Token invalid, throw error requesting user to sso login.
  40. throw AWS.util.error(
  41. new Error('SSO Token refresh failed. Please log in using "aws sso login"'),
  42. { code: 'SSOTokenProviderFailure' }
  43. );
  44. }
  45. };
  46. /**
  47. * Represents token loaded from disk derived from the AWS SSO device grant authorication flow.
  48. *
  49. * ## Using SSO Token Provider
  50. *
  51. * This provider is checked by default in the Node.js environment in TokenProviderChain.
  52. * To use the SSO Token Provider, simply add your SSO Start URL and Region to the
  53. * ~/.aws/config file in the following format:
  54. *
  55. * [default]
  56. * sso_start_url = https://d-abc123.awsapps.com/start
  57. * sso_region = us-east-1
  58. *
  59. * ## Using custom profiles
  60. *
  61. * The SDK supports loading token for separate profiles. This can be done in two ways:
  62. *
  63. * 1. Set the `AWS_PROFILE` environment variable in your process prior to loading the SDK.
  64. * 2. Directly load the AWS.SSOTokenProvider:
  65. *
  66. * ```javascript
  67. * var ssoTokenProvider = new AWS.SSOTokenProvider({profile: 'myprofile'});
  68. * ```
  69. *
  70. * @!macro nobrowser
  71. */
  72. AWS.SSOTokenProvider = AWS.util.inherit(AWS.Token, {
  73. /**
  74. * Expiry window of five minutes.
  75. */
  76. expiryWindow: 5 * 60,
  77. /**
  78. * Creates a new token object from cached access token.
  79. *
  80. * @param options [map] a set of options
  81. * @option options profile [String] (AWS_PROFILE env var or 'default')
  82. * the name of the profile to load.
  83. * @option options callback [Function] (err) Token is eagerly loaded
  84. * by the constructor. When the callback is called with no error, the
  85. * token has been loaded successfully.
  86. */
  87. constructor: function SSOTokenProvider(options) {
  88. AWS.Token.call(this);
  89. options = options || {};
  90. this.expired = true;
  91. this.profile = options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
  92. this.get(options.callback || AWS.util.fn.noop);
  93. },
  94. /**
  95. * Reads sso_start_url from provided profile, and reads token from
  96. * ~/.aws/sso/cache/<sha1-of-utf8-encoded-value-from-sso_start_url>.json
  97. *
  98. * Throws an error if required fields token and expiresAt are missing.
  99. * Throws an error if token has expired and metadata to perform refresh is
  100. * not available.
  101. * Attempts to refresh the token if it's within 5 minutes before expiry time.
  102. *
  103. * @api private
  104. */
  105. load: function load(callback) {
  106. var self = this;
  107. var profiles = iniLoader.loadFrom({ isConfig: true });
  108. var profile = profiles[this.profile] || {};
  109. if (Object.keys(profile).length === 0) {
  110. throw AWS.util.error(
  111. new Error('Profile "' + this.profile + '" not found'),
  112. { code: 'SSOTokenProviderFailure' }
  113. );
  114. } else if (!profile['sso_session']) {
  115. throw AWS.util.error(
  116. new Error('Profile "' + this.profile + '" is missing required property "sso_session".'),
  117. { code: 'SSOTokenProviderFailure' }
  118. );
  119. }
  120. var ssoSessionName = profile['sso_session'];
  121. var ssoSessions = iniLoader.loadSsoSessionsFrom();
  122. var ssoSession = ssoSessions[ssoSessionName];
  123. if (!ssoSession) {
  124. throw AWS.util.error(
  125. new Error('Sso session "' + ssoSessionName + '" not found'),
  126. { code: 'SSOTokenProviderFailure' }
  127. );
  128. } else if (!ssoSession['sso_start_url']) {
  129. throw AWS.util.error(
  130. new Error('Sso session "' + this.profile + '" is missing required property "sso_start_url".'),
  131. { code: 'SSOTokenProviderFailure' }
  132. );
  133. } else if (!ssoSession['sso_region']) {
  134. throw AWS.util.error(
  135. new Error('Sso session "' + this.profile + '" is missing required property "sso_region".'),
  136. { code: 'SSOTokenProviderFailure' }
  137. );
  138. }
  139. var hasher = crypto.createHash('sha1');
  140. var fileName = hasher.update(ssoSessionName).digest('hex') + '.json';
  141. var cachePath = path.join(iniLoader.getHomeDir(), '.aws', 'sso', 'cache', fileName);
  142. var tokenFromCache = JSON.parse(fs.readFileSync(cachePath));
  143. if (!tokenFromCache) {
  144. throw AWS.util.error(
  145. new Error('Cached token not found. Please log in using "aws sso login"'
  146. + ' for profile "' + this.profile + '".'),
  147. { code: 'SSOTokenProviderFailure' }
  148. );
  149. }
  150. validateTokenKey(tokenFromCache, 'accessToken');
  151. validateTokenKey(tokenFromCache, 'expiresAt');
  152. var currentTime = AWS.util.date.getDate().getTime();
  153. var adjustedTime = new Date(currentTime + this.expiryWindow * 1000);
  154. var tokenExpireTime = new Date(tokenFromCache['expiresAt']);
  155. if (tokenExpireTime > adjustedTime) {
  156. // Token is valid and not expired.
  157. self.token = tokenFromCache.accessToken;
  158. self.expireTime = tokenExpireTime;
  159. self.expired = false;
  160. callback(null);
  161. return;
  162. }
  163. // Skip new refresh, if last refresh was done within 30 seconds.
  164. if (currentTime - lastRefreshAttemptTime < 30 * 1000) {
  165. refreshUnsuccessful(currentTime, tokenExpireTime, callback);
  166. return;
  167. }
  168. // Token is in expiry window, refresh from SSOOIDC.createToken() call.
  169. validateTokenKey(tokenFromCache, 'clientId');
  170. validateTokenKey(tokenFromCache, 'clientSecret');
  171. validateTokenKey(tokenFromCache, 'refreshToken');
  172. if (!self.service || self.service.config.region !== ssoSession.sso_region) {
  173. self.service = new AWS.SSOOIDC({ region: ssoSession.sso_region });
  174. }
  175. var params = {
  176. clientId: tokenFromCache.clientId,
  177. clientSecret: tokenFromCache.clientSecret,
  178. refreshToken: tokenFromCache.refreshToken,
  179. grantType: 'refresh_token',
  180. };
  181. lastRefreshAttemptTime = AWS.util.date.getDate().getTime();
  182. self.service.createToken(params, function(err, data) {
  183. if (err || !data) {
  184. refreshUnsuccessful(currentTime, tokenExpireTime, callback);
  185. } else {
  186. try {
  187. validateTokenKey(data, 'accessToken');
  188. validateTokenKey(data, 'expiresIn');
  189. self.expired = false;
  190. self.token = data.accessToken;
  191. self.expireTime = new Date(Date.now() + data.expiresIn * 1000);
  192. callback(null);
  193. try {
  194. // Write updated token data to disk.
  195. tokenFromCache.accessToken = data.accessToken;
  196. tokenFromCache.expiresAt = self.expireTime.toISOString();
  197. tokenFromCache.refreshToken = data.refreshToken;
  198. fs.writeFileSync(cachePath, JSON.stringify(tokenFromCache, null, 2));
  199. } catch (error) {
  200. // Swallow error if unable to write token to file.
  201. }
  202. } catch (error) {
  203. refreshUnsuccessful(currentTime, tokenExpireTime, callback);
  204. }
  205. }
  206. });
  207. },
  208. /**
  209. * Loads the cached access token from disk.
  210. *
  211. * @callback callback function(err)
  212. * Called after the AWS SSO process has been executed. When this
  213. * callback is called with no error, it means that the token information
  214. * has been loaded into the object (as the `token` property).
  215. * @param err [Error] if an error occurred, this value will be filled.
  216. * @see get
  217. */
  218. refresh: function refresh(callback) {
  219. iniLoader.clearCachedFiles();
  220. this.coalesceRefresh(callback || AWS.util.fn.callback);
  221. },
  222. });