123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- var AWS = require('../core');
- var crypto = require('crypto');
- var fs = require('fs');
- var path = require('path');
- var iniLoader = AWS.util.iniLoader;
- // Tracking refresh attempt to ensure refresh is not attempted more than once every 30 seconds.
- var lastRefreshAttemptTime = 0;
- /**
- * Throws error is key is not present in token object.
- *
- * @param token [Object] Object to be validated.
- * @param key [String] The key to be validated on the object.
- */
- var validateTokenKey = function validateTokenKey(token, key) {
- if (!token[key]) {
- throw AWS.util.error(
- new Error('Key "' + key + '" not present in SSO Token'),
- { code: 'SSOTokenProviderFailure' }
- );
- }
- };
- /**
- * Calls callback function with or without error based on provided times in case
- * of unsuccessful refresh.
- *
- * @param currentTime [number] current time in milliseconds since ECMAScript epoch.
- * @param tokenExpireTime [number] token expire time in milliseconds since ECMAScript epoch.
- * @param callback [Function] Callback to call in case of error.
- */
- var refreshUnsuccessful = function refreshUnsuccessful(
- currentTime,
- tokenExpireTime,
- callback
- ) {
- if (tokenExpireTime > currentTime) {
- // Cached token is still valid, return.
- callback(null);
- } else {
- // Token invalid, throw error requesting user to sso login.
- throw AWS.util.error(
- new Error('SSO Token refresh failed. Please log in using "aws sso login"'),
- { code: 'SSOTokenProviderFailure' }
- );
- }
- };
- /**
- * Represents token loaded from disk derived from the AWS SSO device grant authorication flow.
- *
- * ## Using SSO Token Provider
- *
- * This provider is checked by default in the Node.js environment in TokenProviderChain.
- * To use the SSO Token Provider, simply add your SSO Start URL and Region to the
- * ~/.aws/config file in the following format:
- *
- * [default]
- * sso_start_url = https://d-abc123.awsapps.com/start
- * sso_region = us-east-1
- *
- * ## Using custom profiles
- *
- * The SDK supports loading token for separate profiles. This can be done in two ways:
- *
- * 1. Set the `AWS_PROFILE` environment variable in your process prior to loading the SDK.
- * 2. Directly load the AWS.SSOTokenProvider:
- *
- * ```javascript
- * var ssoTokenProvider = new AWS.SSOTokenProvider({profile: 'myprofile'});
- * ```
- *
- * @!macro nobrowser
- */
- AWS.SSOTokenProvider = AWS.util.inherit(AWS.Token, {
- /**
- * Expiry window of five minutes.
- */
- expiryWindow: 5 * 60,
- /**
- * Creates a new token object from cached access token.
- *
- * @param options [map] a set of options
- * @option options profile [String] (AWS_PROFILE env var or 'default')
- * the name of the profile to load.
- * @option options callback [Function] (err) Token is eagerly loaded
- * by the constructor. When the callback is called with no error, the
- * token has been loaded successfully.
- */
- constructor: function SSOTokenProvider(options) {
- AWS.Token.call(this);
- options = options || {};
- this.expired = true;
- this.profile = options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
- this.get(options.callback || AWS.util.fn.noop);
- },
- /**
- * Reads sso_start_url from provided profile, and reads token from
- * ~/.aws/sso/cache/<sha1-of-utf8-encoded-value-from-sso_start_url>.json
- *
- * Throws an error if required fields token and expiresAt are missing.
- * Throws an error if token has expired and metadata to perform refresh is
- * not available.
- * Attempts to refresh the token if it's within 5 minutes before expiry time.
- *
- * @api private
- */
- load: function load(callback) {
- var self = this;
- var profiles = iniLoader.loadFrom({ isConfig: true });
- var profile = profiles[this.profile] || {};
- if (Object.keys(profile).length === 0) {
- throw AWS.util.error(
- new Error('Profile "' + this.profile + '" not found'),
- { code: 'SSOTokenProviderFailure' }
- );
- } else if (!profile['sso_session']) {
- throw AWS.util.error(
- new Error('Profile "' + this.profile + '" is missing required property "sso_session".'),
- { code: 'SSOTokenProviderFailure' }
- );
- }
- var ssoSessionName = profile['sso_session'];
- var ssoSessions = iniLoader.loadSsoSessionsFrom();
- var ssoSession = ssoSessions[ssoSessionName];
- if (!ssoSession) {
- throw AWS.util.error(
- new Error('Sso session "' + ssoSessionName + '" not found'),
- { code: 'SSOTokenProviderFailure' }
- );
- } else if (!ssoSession['sso_start_url']) {
- throw AWS.util.error(
- new Error('Sso session "' + this.profile + '" is missing required property "sso_start_url".'),
- { code: 'SSOTokenProviderFailure' }
- );
- } else if (!ssoSession['sso_region']) {
- throw AWS.util.error(
- new Error('Sso session "' + this.profile + '" is missing required property "sso_region".'),
- { code: 'SSOTokenProviderFailure' }
- );
- }
- var hasher = crypto.createHash('sha1');
- var fileName = hasher.update(ssoSessionName).digest('hex') + '.json';
- var cachePath = path.join(iniLoader.getHomeDir(), '.aws', 'sso', 'cache', fileName);
- var tokenFromCache = JSON.parse(fs.readFileSync(cachePath));
- if (!tokenFromCache) {
- throw AWS.util.error(
- new Error('Cached token not found. Please log in using "aws sso login"'
- + ' for profile "' + this.profile + '".'),
- { code: 'SSOTokenProviderFailure' }
- );
- }
- validateTokenKey(tokenFromCache, 'accessToken');
- validateTokenKey(tokenFromCache, 'expiresAt');
- var currentTime = AWS.util.date.getDate().getTime();
- var adjustedTime = new Date(currentTime + this.expiryWindow * 1000);
- var tokenExpireTime = new Date(tokenFromCache['expiresAt']);
- if (tokenExpireTime > adjustedTime) {
- // Token is valid and not expired.
- self.token = tokenFromCache.accessToken;
- self.expireTime = tokenExpireTime;
- self.expired = false;
- callback(null);
- return;
- }
- // Skip new refresh, if last refresh was done within 30 seconds.
- if (currentTime - lastRefreshAttemptTime < 30 * 1000) {
- refreshUnsuccessful(currentTime, tokenExpireTime, callback);
- return;
- }
- // Token is in expiry window, refresh from SSOOIDC.createToken() call.
- validateTokenKey(tokenFromCache, 'clientId');
- validateTokenKey(tokenFromCache, 'clientSecret');
- validateTokenKey(tokenFromCache, 'refreshToken');
- if (!self.service || self.service.config.region !== ssoSession.sso_region) {
- self.service = new AWS.SSOOIDC({ region: ssoSession.sso_region });
- }
- var params = {
- clientId: tokenFromCache.clientId,
- clientSecret: tokenFromCache.clientSecret,
- refreshToken: tokenFromCache.refreshToken,
- grantType: 'refresh_token',
- };
- lastRefreshAttemptTime = AWS.util.date.getDate().getTime();
- self.service.createToken(params, function(err, data) {
- if (err || !data) {
- refreshUnsuccessful(currentTime, tokenExpireTime, callback);
- } else {
- try {
- validateTokenKey(data, 'accessToken');
- validateTokenKey(data, 'expiresIn');
- self.expired = false;
- self.token = data.accessToken;
- self.expireTime = new Date(Date.now() + data.expiresIn * 1000);
- callback(null);
- try {
- // Write updated token data to disk.
- tokenFromCache.accessToken = data.accessToken;
- tokenFromCache.expiresAt = self.expireTime.toISOString();
- tokenFromCache.refreshToken = data.refreshToken;
- fs.writeFileSync(cachePath, JSON.stringify(tokenFromCache, null, 2));
- } catch (error) {
- // Swallow error if unable to write token to file.
- }
- } catch (error) {
- refreshUnsuccessful(currentTime, tokenExpireTime, callback);
- }
- }
- });
- },
- /**
- * Loads the cached access token from disk.
- *
- * @callback callback function(err)
- * Called after the AWS SSO process has been executed. When this
- * callback is called with no error, it means that the token information
- * has been loaded into the object (as the `token` property).
- * @param err [Error] if an error occurred, this value will be filled.
- * @see get
- */
- refresh: function refresh(callback) {
- iniLoader.clearCachedFiles();
- this.coalesceRefresh(callback || AWS.util.fn.callback);
- },
- });
|