Skip to content

Commit

Permalink
feat: Token file web identity credentials (#2737)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Aug 23, 2019
1 parent f3baf93 commit 75690eb
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changes/next-release/feature-Credentials-e1371a8c.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "Credentials",
"description": "Added TokenFileWebIdentityCredentials"
}
20 changes: 1 addition & 19 deletions lib/credentials/process_credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,7 @@ AWS.ProcessCredentials = AWS.util.inherit(AWS.Credentials, {
load: function load(callback) {
var self = this;
try {
var profiles = {};
var profilesFromConfig = {};
if (process.env[AWS.util.configOptInEnv]) {
var profilesFromConfig = iniLoader.loadFrom({
isConfig: true,
filename: process.env[AWS.util.sharedConfigFileEnv]
});
}
var profilesFromCreds = iniLoader.loadFrom({
filename: this.filename ||
(process.env[AWS.util.configOptInEnv] && process.env[AWS.util.sharedCredentialsFileEnv])
});
for (var i = 0, profileNames = Object.keys(profilesFromCreds); i < profileNames.length; i++) {
profiles[profileNames[i]] = profilesFromCreds[profileNames[i]];
}
// load after profilesFromCreds to prefer profilesFromConfig
for (var i = 0, profileNames = Object.keys(profilesFromConfig); i < profileNames.length; i++) {
profiles[profileNames[i]] = profilesFromConfig[profileNames[i]];
}
var profiles = AWS.util.getProfilesFromSharedConfig(iniLoader, this.filename);
var profile = profiles[this.profile] || {};

if (Object.keys(profile).length === 0) {
Expand Down
19 changes: 1 addition & 18 deletions lib/credentials/shared_ini_file_credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,7 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, {
load: function load(callback) {
var self = this;
try {
var profiles = {};
var profilesFromConfig = {};
if (process.env[AWS.util.configOptInEnv]) {
var profilesFromConfig = iniLoader.loadFrom({
isConfig: true,
filename: process.env[AWS.util.sharedConfigFileEnv]
});
}
var profilesFromCreds = iniLoader.loadFrom({
filename: this.filename ||
(process.env[AWS.util.configOptInEnv] && process.env[AWS.util.sharedCredentialsFileEnv])
});
for (var i = 0, profileNames = Object.keys(profilesFromConfig); i < profileNames.length; i++) {
profiles[profileNames[i]] = profilesFromConfig[profileNames[i]];
}
for (var i = 0, profileNames = Object.keys(profilesFromCreds); i < profileNames.length; i++) {
profiles[profileNames[i]] = profilesFromCreds[profileNames[i]];
}
var profiles = AWS.util.getProfilesFromSharedConfig(iniLoader, this.filename);
var profile = profiles[this.profile] || {};

if (Object.keys(profile).length === 0) {
Expand Down
14 changes: 14 additions & 0 deletions lib/credentials/token_file_web_identity_credentials.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Credentials} from '../credentials';
import {AWSError} from '../error';
import {ConfigurationOptions} from '../config';
export class TokenFileWebIdentityCredentials extends Credentials {
/**
* Creates a new credentials object with optional configuraion.
* @param {Object} clientConfig - a map of configuration options to pass to the underlying STS client.
*/
constructor(clientConfig?: ConfigurationOptions);
/**
* Refreshes credentials using AWS.STS.assumeRoleWithWebIdentity().
*/
refresh(callback: (err: AWSError) => void): void;
}
163 changes: 163 additions & 0 deletions lib/credentials/token_file_web_identity_credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
var AWS = require('../core');
var fs = require('fs');
var STS = require('../../clients/sts');
var iniLoader = AWS.util.iniLoader;

/**
* Represents OIDC credentials from a file on disk
* If the credentials expire, the SDK can {refresh} the credentials
* from the file.
*
* ## Using the web identity token file
*
* This provider is checked by default in the Node.js environment. To use
* the provider simply add your OIDC token to a file (ASCII encoding) and
* share the filename in either AWS_WEB_IDENTITY_TOKEN_FILE environment
* variable or web_identity_token_file shared config variable
*
* The file contains encoded OIDC token and the characters are
* ASCII encoded. OIDC tokens are JSON Web Tokens (JWT).
* JWT's are 3 base64 encoded strings joined by the '.' character.
*
* This class will read filename from AWS_WEB_IDENTITY_TOKEN_FILE
* environment variable or web_identity_token_file shared config variable,
* and get the OIDC token from filename.
* It will also read IAM role to be assumed from AWS_IAM_ROLE_ARN
* environment variable or role_arn shared config variable.
* This provider gets credetials using the {AWS.STS.assumeRoleWithWebIdentity}
* service operation
*
* @!macro nobrowser
*/
AWS.TokenFileWebIdentityCredentials = AWS.util.inherit(AWS.Credentials, {

/**
* @example Creating a new credentials object
* AWS.config.credentials = new AWS.TokenFileWebIdentityCredentials(
* // optionally provide configuration to apply to the underlying AWS.STS service client
* // if configuration is not provided, then configuration will be pulled from AWS.config
* {
* // specify timeout options
* httpOptions: {
* timeout: 100
* }
* });
* @see AWS.Config
*/
constructor: function TokenFileWebIdentityCredentials(clientConfig) {
AWS.Credentials.call(this);
this.data = null;
this.clientConfig = AWS.util.copy(clientConfig || {});
},

/**
* Returns params from environment variables
*
* @api private
*/
getParamsFromEnv: function getParamsFromEnv() {
var ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE',
ENV_ROLE_ARN = 'AWS_IAM_ROLE_ARN';
if (process.env[ENV_TOKEN_FILE] && process.env[ENV_ROLE_ARN]) {
return {
envTokenFile: process.env[ENV_TOKEN_FILE],
roleArn: process.env[ENV_ROLE_ARN],
roleSessionName: process.env['AWS_IAM_ROLE_SESSION_NAME']
};
}
},

/**
* Returns params from shared config variables
*
* @api private
*/
getParamsFromSharedConfig: function getParamsFromSharedConfig() {
var profiles = AWS.util.getProfilesFromSharedConfig(iniLoader);
var profileName = process.env.AWS_PROFILE || AWS.util.defaultProfile;
var profile = profiles[profileName] || {};

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

return {
envTokenFile: profile['web_identity_token_file'],
roleArn: profile['role_arn'],
roleSessionName: profile['role_session_name']
};
},

/**
* Refreshes credentials using {AWS.STS.assumeRoleWithWebIdentity}
*
* @callback callback function(err)
* Called when the STS service responds (or fails). 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 AWS.Credentials.get
*/
refresh: function refresh(callback) {
this.coalesceRefresh(callback || AWS.util.fn.callback);
},

/**
* @api private
*/
load: function load(callback) {
var self = this;
try {
var params = self.getParamsFromEnv();
if (!params) {
params = self.getParamsFromSharedConfig();
}
if (params) {
var oidcToken = fs.readFileSync(params.envTokenFile, {encoding: 'ascii'});
if (self.service) {
self.service.config.params.WebIdentityToken = oidcToken;
} else {
self.createClients({
WebIdentityToken: oidcToken,
RoleArn: params.roleArn,
RoleSessionName: params.roleSessionName || 'token-file-web-identity'
});
}
self.service.assumeRoleWithWebIdentity(function (err, data) {
self.data = null;
if (!err) {
self.data = data;
self.service.credentialsFrom(data, self);
}
callback(err);
});
}
} catch (err) {
callback(err);
}
},

/**
* @api private
*/
createClients: function(params) {
if (!this.service) {
var stsConfig = AWS.util.merge({}, this.clientConfig);
stsConfig.params = params;
this.service = new STS(stsConfig);

// Retry in case of IDPCommunicationErrorException or InvalidIdentityToken
this.service.retryableError = function(error) {
if (error.code === 'IDPCommunicationErrorException' || error.code === 'InvalidIdentityToken') {
return true;
} else {
return AWS.Service.prototype.retryableError.call(this, error);
}
};
}
}
});
2 changes: 2 additions & 0 deletions lib/node_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require('./http/node');
require('./shared-ini/ini-loader');

// Load custom credential providers
require('./credentials/token_file_web_identity_credentials');
require('./credentials/ec2_metadata_credentials');
require('./credentials/remote_credentials');
require('./credentials/ecs_credentials');
Expand All @@ -63,6 +64,7 @@ AWS.CredentialProviderChain.defaultProviders = [
function () { return new AWS.SharedIniFileCredentials(); },
function () { return new AWS.ECSCredentials(); },
function () { return new AWS.ProcessCredentials(); },
function () { return new AWS.TokenFileWebIdentityCredentials(); },
function () { return new AWS.EC2MetadataCredentials(); }
];

Expand Down
22 changes: 22 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,28 @@ var util = {
return operation.input.members[operation.input.payload];
},

getProfilesFromSharedConfig: function getProfilesFromSharedConfig(iniLoader, filename) {
var profiles = {};
var profilesFromConfig = {};
if (process.env[util.configOptInEnv]) {
var profilesFromConfig = iniLoader.loadFrom({
isConfig: true,
filename: process.env[util.sharedConfigFileEnv]
});
}
var profilesFromCreds = iniLoader.loadFrom({
filename: filename ||
(process.env[util.configOptInEnv] && process.env[util.sharedCredentialsFileEnv])
});
for (var i = 0, profileNames = Object.keys(profilesFromConfig); i < profileNames.length; i++) {
profiles[profileNames[i]] = profilesFromConfig[profileNames[i]];
}
for (var i = 0, profileNames = Object.keys(profilesFromCreds); i < profileNames.length; i++) {
profiles[profileNames[i]] = profilesFromCreds[profileNames[i]];
}
return profiles;
},

/**
* @api private
*/
Expand Down
103 changes: 103 additions & 0 deletions test/credentials.spec.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 75690eb

Please sign in to comment.