Skip to content

Commit 7888e6f

Browse files
Add AWS SSO credentials provider (#4047)
Co-authored-by: Eduardo Rodrigues <[email protected]>
1 parent 222a8ac commit 7888e6f

File tree

8 files changed

+434
-0
lines changed

8 files changed

+434
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "credentials",
4+
"description": "Add AWS SSO Credentials Provider"
5+
}

lib/core.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {EnvironmentCredentials} from './credentials/environment_credentials';
1010
export {FileSystemCredentials} from './credentials/file_system_credentials';
1111
export {SAMLCredentials} from './credentials/saml_credentials';
1212
export {SharedIniFileCredentials} from './credentials/shared_ini_file_credentials';
13+
export {SsoCredentials} from './credentials/sso_credentials';
1314
export {ProcessCredentials} from './credentials/process_credentials';
1415
export {TemporaryCredentials} from './credentials/temporary_credentials';
1516
export {ChainableTemporaryCredentials} from './credentials/chainable_temporary_credentials';

lib/credentials/credential_provider_chain.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ AWS.CredentialProviderChain = AWS.util.inherit(AWS.Credentials, {
152152
* AWS.CredentialProviderChain.defaultProviders = [
153153
* function () { return new AWS.EnvironmentCredentials('AWS'); },
154154
* function () { return new AWS.EnvironmentCredentials('AMAZON'); },
155+
* function () { return new AWS.SsoCredentials(); },
155156
* function () { return new AWS.SharedIniFileCredentials(); },
156157
* function () { return new AWS.ECSCredentials(); },
157158
* function () { return new AWS.ProcessCredentials(); },

lib/credentials/sso_credentials.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Credentials} from '../credentials';
2+
import SSO = require('../../clients/sso');
3+
export class SsoCredentials extends Credentials {
4+
/**
5+
* Creates a new SsoCredentials object.
6+
*/
7+
constructor(options?: SsoCredentialsOptions);
8+
}
9+
10+
interface SsoCredentialsOptions {
11+
profile?: string;
12+
filename?: string;
13+
ssoClient?: SSO;
14+
}

lib/credentials/sso_credentials.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
var AWS = require('../core');
2+
var path = require('path');
3+
var crypto = require('crypto');
4+
var iniLoader = AWS.util.iniLoader;
5+
6+
/**
7+
* Represents credentials from sso.getRoleCredentials API for
8+
* `sso_*` values defined in shared credentials file.
9+
*
10+
* ## Using SSO credentials
11+
*
12+
* The credentials file must specify the information below to use sso:
13+
*
14+
* [default]
15+
* sso_account_id = 012345678901
16+
* sso_region = us-east-1
17+
* sso_role_name = SampleRole
18+
* sso_start_url = https://d-abc123.awsapps.com/start
19+
*
20+
* This information will be automatically added to your shared credentials file by running
21+
* `aws configure sso`.
22+
*
23+
* ## Using custom profiles
24+
*
25+
* The SDK supports loading credentials for separate profiles. This can be done
26+
* in two ways:
27+
*
28+
* 1. Set the `AWS_PROFILE` environment variable in your process prior to
29+
* loading the SDK.
30+
* 2. Directly load the AWS.SsoCredentials provider:
31+
*
32+
* ```javascript
33+
* var creds = new AWS.SsoCredentials({profile: 'myprofile'});
34+
* AWS.config.credentials = creds;
35+
* ```
36+
*
37+
* @!macro nobrowser
38+
*/
39+
AWS.SsoCredentials = AWS.util.inherit(AWS.Credentials, {
40+
/**
41+
* Creates a new SsoCredentials object.
42+
*
43+
* @param options [map] a set of options
44+
* @option options profile [String] (AWS_PROFILE env var or 'default')
45+
* the name of the profile to load.
46+
* @option options filename [String] ('~/.aws/credentials' or defined by
47+
* AWS_SHARED_CREDENTIALS_FILE process env var)
48+
* the filename to use when loading credentials.
49+
* @option options callback [Function] (err) Credentials are eagerly loaded
50+
* by the constructor. When the callback is called with no error, the
51+
* credentials have been loaded successfully.
52+
*/
53+
constructor: function SsoCredentials(options) {
54+
AWS.Credentials.call(this);
55+
56+
options = options || {};
57+
this.errorCode = 'SsoCredentialsProviderFailure';
58+
this.expired = true;
59+
60+
this.filename = options.filename;
61+
this.profile = options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
62+
this.service = options.ssoClient;
63+
this.get(options.callback || AWS.util.fn.noop);
64+
},
65+
66+
/**
67+
* @api private
68+
*/
69+
load: function load(callback) {
70+
/**
71+
* The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
72+
* This is needed because server side may have invalidated the token before the defined expiration date.
73+
*
74+
* @internal
75+
*/
76+
var EXPIRE_WINDOW_MS = 15 * 60 * 1000;
77+
var self = this;
78+
try {
79+
var profiles = AWS.util.getProfilesFromSharedConfig(iniLoader, this.filename);
80+
var profile = profiles[this.profile] || {};
81+
82+
if (Object.keys(profile).length === 0) {
83+
throw AWS.util.error(
84+
new Error('Profile ' + this.profile + ' not found'),
85+
{ code: self.errorCode }
86+
);
87+
}
88+
89+
if (!profile.sso_start_url || !profile.sso_account_id || !profile.sso_region || !profile.sso_role_name) {
90+
throw AWS.util.error(
91+
new Error('Profile ' + this.profile + ' does not have valid SSO credentials. Required parameters "sso_account_id", "sso_region", ' +
92+
'"sso_role_name", "sso_start_url". Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html'),
93+
{ code: self.errorCode }
94+
);
95+
}
96+
97+
var hasher = crypto.createHash('sha1');
98+
var fileName = hasher.update(profile.sso_start_url).digest('hex') + '.json';
99+
100+
var cachePath = path.join(
101+
iniLoader.getHomeDir(),
102+
'.aws',
103+
'sso',
104+
'cache',
105+
fileName
106+
);
107+
var cacheFile = AWS.util.readFileSync(cachePath);
108+
var cacheContent = null;
109+
if (cacheFile) {
110+
cacheContent = JSON.parse(cacheFile);
111+
}
112+
113+
if (!cacheContent) {
114+
throw AWS.util.error(
115+
new Error('Cached credentials not found under ' + this.profile + ' profile. Please make sure you log in with aws sso login first'),
116+
{ code: self.errorCode }
117+
);
118+
}
119+
120+
if (!cacheContent.startUrl || !cacheContent.region || !cacheContent.accessToken || !cacheContent.expiresAt) {
121+
throw AWS.util.error(
122+
new Error('Cached credentials are missing required properties. Try running aws sso login.')
123+
);
124+
}
125+
126+
if (new Date(cacheContent.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) {
127+
throw AWS.util.error(new Error(
128+
'The SSO session associated with this profile has expired. To refresh this SSO session run aws sso login with the corresponding profile.'
129+
));
130+
}
131+
132+
if (!self.service || self.service.config.region !== profile.sso_region) {
133+
self.service = new AWS.SSO({ region: profile.sso_region });
134+
}
135+
var request = {
136+
accessToken: cacheContent.accessToken,
137+
accountId: profile.sso_account_id,
138+
roleName: profile.sso_role_name,
139+
};
140+
self.service.getRoleCredentials(request, function(err, data) {
141+
if (err || !data || !data.roleCredentials) {
142+
callback(AWS.util.error(
143+
err || new Error('Please log in using "aws sso login"'),
144+
{ code: self.errorCode }
145+
), null);
146+
} else if (!data.roleCredentials.accessKeyId || !data.roleCredentials.secretAccessKey || !data.roleCredentials.sessionToken || !data.roleCredentials.expiration) {
147+
throw AWS.util.error(new Error(
148+
'SSO returns an invalid temporary credential.'
149+
));
150+
} else {
151+
self.expired = false;
152+
self.accessKeyId = data.roleCredentials.accessKeyId;
153+
self.secretAccessKey = data.roleCredentials.secretAccessKey;
154+
self.sessionToken = data.roleCredentials.sessionToken;
155+
self.expireTime = new Date(data.roleCredentials.expiration);
156+
callback(null);
157+
}
158+
});
159+
} catch (err) {
160+
callback(err);
161+
}
162+
},
163+
164+
/**
165+
* Loads the credentials from the AWS SSO process
166+
*
167+
* @callback callback function(err)
168+
* Called after the AWS SSO process has been executed. When this
169+
* callback is called with no error, it means that the credentials
170+
* information has been loaded into the object (as the `accessKeyId`,
171+
* `secretAccessKey`, and `sessionToken` properties).
172+
* @param err [Error] if an error occurred, this value will be filled
173+
* @see get
174+
*/
175+
refresh: function refresh(callback) {
176+
iniLoader.clearCachedFiles();
177+
this.coalesceRefresh(callback || AWS.util.fn.callback);
178+
},
179+
});

lib/node_loader.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ require('./credentials/environment_credentials');
8585
require('./credentials/file_system_credentials');
8686
require('./credentials/shared_ini_file_credentials');
8787
require('./credentials/process_credentials');
88+
require('./credentials/sso_credentials');
8889

8990
// Setup default chain providers
9091
// If this changes, please update documentation for
@@ -93,6 +94,7 @@ require('./credentials/process_credentials');
9394
AWS.CredentialProviderChain.defaultProviders = [
9495
function () { return new AWS.EnvironmentCredentials('AWS'); },
9596
function () { return new AWS.EnvironmentCredentials('AMAZON'); },
97+
function () { return new AWS.SsoCredentials(); },
9698
function () { return new AWS.SharedIniFileCredentials(); },
9799
function () { return new AWS.ECSCredentials(); },
98100
function () { return new AWS.ProcessCredentials(); },

scripts/region-checker/allowlist.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ var allowlist = {
1616
'/credentials/shared_ini_file_credentials.js': [
1717
4,
1818
],
19+
'/credentials/sso_credentials.js': [
20+
15,
21+
],
1922
'/http.js': [
2023
5
2124
],

0 commit comments

Comments
 (0)