Skip to content

Commit

Permalink
Add AWS SSO credentials provider (#4047)
Browse files Browse the repository at this point in the history
Co-authored-by: Eduardo Rodrigues <[email protected]>
  • Loading branch information
comcalvi and eduardomourar authored Mar 11, 2022
1 parent 222a8ac commit 7888e6f
Show file tree
Hide file tree
Showing 8 changed files with 434 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changes/next-release/feature-credentials-4c9003e0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "credentials",
"description": "Add AWS SSO Credentials Provider"
}
1 change: 1 addition & 0 deletions lib/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {EnvironmentCredentials} from './credentials/environment_credentials';
export {FileSystemCredentials} from './credentials/file_system_credentials';
export {SAMLCredentials} from './credentials/saml_credentials';
export {SharedIniFileCredentials} from './credentials/shared_ini_file_credentials';
export {SsoCredentials} from './credentials/sso_credentials';
export {ProcessCredentials} from './credentials/process_credentials';
export {TemporaryCredentials} from './credentials/temporary_credentials';
export {ChainableTemporaryCredentials} from './credentials/chainable_temporary_credentials';
Expand Down
1 change: 1 addition & 0 deletions lib/credentials/credential_provider_chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ AWS.CredentialProviderChain = AWS.util.inherit(AWS.Credentials, {
* AWS.CredentialProviderChain.defaultProviders = [
* function () { return new AWS.EnvironmentCredentials('AWS'); },
* function () { return new AWS.EnvironmentCredentials('AMAZON'); },
* function () { return new AWS.SsoCredentials(); },
* function () { return new AWS.SharedIniFileCredentials(); },
* function () { return new AWS.ECSCredentials(); },
* function () { return new AWS.ProcessCredentials(); },
Expand Down
14 changes: 14 additions & 0 deletions lib/credentials/sso_credentials.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Credentials} from '../credentials';
import SSO = require('../../clients/sso');
export class SsoCredentials extends Credentials {
/**
* Creates a new SsoCredentials object.
*/
constructor(options?: SsoCredentialsOptions);
}

interface SsoCredentialsOptions {
profile?: string;
filename?: string;
ssoClient?: SSO;
}
179 changes: 179 additions & 0 deletions lib/credentials/sso_credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
var AWS = require('../core');
var path = require('path');
var crypto = require('crypto');
var iniLoader = AWS.util.iniLoader;

/**
* Represents credentials from sso.getRoleCredentials API for
* `sso_*` values defined in shared credentials file.
*
* ## Using SSO credentials
*
* The credentials file must specify the information below to use sso:
*
* [default]
* sso_account_id = 012345678901
* sso_region = us-east-1
* sso_role_name = SampleRole
* sso_start_url = https://d-abc123.awsapps.com/start
*
* This information will be automatically added to your shared credentials file by running
* `aws configure sso`.
*
* ## Using custom profiles
*
* The SDK supports loading credentials 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.SsoCredentials provider:
*
* ```javascript
* var creds = new AWS.SsoCredentials({profile: 'myprofile'});
* AWS.config.credentials = creds;
* ```
*
* @!macro nobrowser
*/
AWS.SsoCredentials = AWS.util.inherit(AWS.Credentials, {
/**
* Creates a new SsoCredentials object.
*
* @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 filename [String] ('~/.aws/credentials' or defined by
* AWS_SHARED_CREDENTIALS_FILE process env var)
* the filename to use when loading credentials.
* @option options callback [Function] (err) Credentials are eagerly loaded
* by the constructor. When the callback is called with no error, the
* credentials have been loaded successfully.
*/
constructor: function SsoCredentials(options) {
AWS.Credentials.call(this);

options = options || {};
this.errorCode = 'SsoCredentialsProviderFailure';
this.expired = true;

this.filename = options.filename;
this.profile = options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
this.service = options.ssoClient;
this.get(options.callback || AWS.util.fn.noop);
},

/**
* @api private
*/
load: function load(callback) {
/**
* The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
* This is needed because server side may have invalidated the token before the defined expiration date.
*
* @internal
*/
var EXPIRE_WINDOW_MS = 15 * 60 * 1000;
var self = this;
try {
var profiles = AWS.util.getProfilesFromSharedConfig(iniLoader, this.filename);
var profile = profiles[this.profile] || {};

if (Object.keys(profile).length === 0) {
throw AWS.util.error(
new Error('Profile ' + this.profile + ' not found'),
{ code: self.errorCode }
);
}

if (!profile.sso_start_url || !profile.sso_account_id || !profile.sso_region || !profile.sso_role_name) {
throw AWS.util.error(
new Error('Profile ' + this.profile + ' does not have valid SSO credentials. Required parameters "sso_account_id", "sso_region", ' +
'"sso_role_name", "sso_start_url". Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html'),
{ code: self.errorCode }
);
}

var hasher = crypto.createHash('sha1');
var fileName = hasher.update(profile.sso_start_url).digest('hex') + '.json';

var cachePath = path.join(
iniLoader.getHomeDir(),
'.aws',
'sso',
'cache',
fileName
);
var cacheFile = AWS.util.readFileSync(cachePath);
var cacheContent = null;
if (cacheFile) {
cacheContent = JSON.parse(cacheFile);
}

if (!cacheContent) {
throw AWS.util.error(
new Error('Cached credentials not found under ' + this.profile + ' profile. Please make sure you log in with aws sso login first'),
{ code: self.errorCode }
);
}

if (!cacheContent.startUrl || !cacheContent.region || !cacheContent.accessToken || !cacheContent.expiresAt) {
throw AWS.util.error(
new Error('Cached credentials are missing required properties. Try running aws sso login.')
);
}

if (new Date(cacheContent.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) {
throw AWS.util.error(new Error(
'The SSO session associated with this profile has expired. To refresh this SSO session run aws sso login with the corresponding profile.'
));
}

if (!self.service || self.service.config.region !== profile.sso_region) {
self.service = new AWS.SSO({ region: profile.sso_region });
}
var request = {
accessToken: cacheContent.accessToken,
accountId: profile.sso_account_id,
roleName: profile.sso_role_name,
};
self.service.getRoleCredentials(request, function(err, data) {
if (err || !data || !data.roleCredentials) {
callback(AWS.util.error(
err || new Error('Please log in using "aws sso login"'),
{ code: self.errorCode }
), null);
} else if (!data.roleCredentials.accessKeyId || !data.roleCredentials.secretAccessKey || !data.roleCredentials.sessionToken || !data.roleCredentials.expiration) {
throw AWS.util.error(new Error(
'SSO returns an invalid temporary credential.'
));
} else {
self.expired = false;
self.accessKeyId = data.roleCredentials.accessKeyId;
self.secretAccessKey = data.roleCredentials.secretAccessKey;
self.sessionToken = data.roleCredentials.sessionToken;
self.expireTime = new Date(data.roleCredentials.expiration);
callback(null);
}
});
} catch (err) {
callback(err);
}
},

/**
* Loads the credentials from the AWS SSO process
*
* @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 credentials
* information has been loaded into the object (as the `accessKeyId`,
* `secretAccessKey`, and `sessionToken` properties).
* @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);
},
});
2 changes: 2 additions & 0 deletions lib/node_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ require('./credentials/environment_credentials');
require('./credentials/file_system_credentials');
require('./credentials/shared_ini_file_credentials');
require('./credentials/process_credentials');
require('./credentials/sso_credentials');

// Setup default chain providers
// If this changes, please update documentation for
Expand All @@ -93,6 +94,7 @@ require('./credentials/process_credentials');
AWS.CredentialProviderChain.defaultProviders = [
function () { return new AWS.EnvironmentCredentials('AWS'); },
function () { return new AWS.EnvironmentCredentials('AMAZON'); },
function () { return new AWS.SsoCredentials(); },
function () { return new AWS.SharedIniFileCredentials(); },
function () { return new AWS.ECSCredentials(); },
function () { return new AWS.ProcessCredentials(); },
Expand Down
3 changes: 3 additions & 0 deletions scripts/region-checker/allowlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ var allowlist = {
'/credentials/shared_ini_file_credentials.js': [
4,
],
'/credentials/sso_credentials.js': [
15,
],
'/http.js': [
5
],
Expand Down
Loading

0 comments on commit 7888e6f

Please sign in to comment.