-
Notifications
You must be signed in to change notification settings - Fork 93
/
script.js
266 lines (236 loc) · 11 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// Global variables
var FileName = 'credentials';
var ApplySessionDuration = true;
var RoleArns = {};
var LF = '\n';
// When this background process starts, load variables from chrome storage
// from saved Extension Options
loadItemsFromStorage();
// Additionaly on start of the background process it is checked if this extension can be activated
chrome.storage.sync.get({
// The default is activated
Activated: true
}, function(item) {
if (item.Activated) addOnBeforeRequestEventListener();
});
// Additionaly on start of the background process it is checked if a new version of the plugin is installed.
// If so, show the user the changelog
// var thisVersion = chrome.runtime.getManifest().version;
chrome.runtime.onInstalled.addListener(function(details){
if(details.reason == "install" || details.reason == "update"){
// Open a new tab to show changelog html page
chrome.tabs.create({url: "../options/changelog.html"});
}
});
// Function to be called when this extension is activated.
// This adds an EventListener for each request to signin.aws.amazon.com
function addOnBeforeRequestEventListener() {
if (chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestEvent)) {
console.log("ERROR: onBeforeRequest EventListener could not be added, because onBeforeRequest already has an EventListener.");
} else {
chrome.webRequest.onBeforeRequest.addListener(
onBeforeRequestEvent,
{urls: ["https://signin.aws.amazon.com/saml"]},
["requestBody"]
);
}
}
// Function to be called when this extension is de-actived
// by unchecking the activation checkbox on the popup page
function removeOnBeforeRequestEventListener() {
chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequestEvent);
}
// Callback function for the webRequest OnBeforeRequest EventListener
// This function runs on each request to https://signin.aws.amazon.com/saml
function onBeforeRequestEvent(details) {
// Decode base64 SAML assertion in the request
var samlXmlDoc = "";
var formDataPayload = undefined;
if (details.requestBody.formData) {
samlXmlDoc = decodeURIComponent(unescape(window.atob(details.requestBody.formData.SAMLResponse[0])));
} else if (details.requestBody.raw) {
var combined = new ArrayBuffer(0);
details.requestBody.raw.forEach(function(element) {
var tmp = new Uint8Array(combined.byteLength + element.bytes.byteLength);
tmp.set( new Uint8Array(combined), 0 );
tmp.set( new Uint8Array(element.bytes),combined.byteLength );
combined = tmp.buffer;
});
var combinedView = new DataView(combined);
var decoder = new TextDecoder('utf-8');
formDataPayload = new URLSearchParams(decoder.decode(combinedView));
samlXmlDoc = decodeURIComponent(unescape(window.atob(formDataPayload.get('SAMLResponse'))))
}
// Convert XML String to DOM
parser = new DOMParser()
domDoc = parser.parseFromString(samlXmlDoc, "text/xml");
// Get a list of claims (= AWS roles) from the SAML assertion
var roleDomNodes = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes
// Parse the PrincipalArn and the RoleArn from the SAML Assertion.
var PrincipalArn = '';
var RoleArn = '';
var SAMLAssertion = undefined;
var SessionDuration = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/SessionDuration"]')[0]
var hasRoleIndex = false;
var roleIndex = undefined;
if (details.requestBody.formData) {
SAMLAssertion = details.requestBody.formData.SAMLResponse[0];
if ("roleIndex" in details.requestBody.formData) {
hasRoleIndex = true;
roleIndex = details.requestBody.formData.roleIndex[0];
}
} else if (formDataPayload) {
SAMLAssertion = formDataPayload.get('SAMLResponse');
roleIndex = formDataPayload.get('roleIndex');
hasRoleIndex = roleIndex != undefined;
}
// Only set the SessionDuration if it was supplied by the SAML provider and
// when the user has configured to use this feature.
if (SessionDuration !== undefined && ApplySessionDuration) {
SessionDuration = Number(SessionDuration.firstElementChild.textContent)
} else {
SessionDuration = null;
}
// Change newline sequence when client is on Windows
if (navigator.userAgent.indexOf('Windows') !== -1) {
LF = '\r\n'
}
// If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume
if (roleDomNodes.length > 1 && hasRoleIndex) {
for (i = 0; i < roleDomNodes.length; i++) {
var nodeValue = roleDomNodes[i].innerHTML;
if (nodeValue.indexOf(roleIndex) > -1) {
// This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call
// The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments.
extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion, SessionDuration)
}
}
}
// If there is just 1 role in the claim there will be no 'roleIndex' in the form data.
else if (roleDomNodes.length == 1) {
// When there is just 1 role in the claim, use these details for the assumeRoleWithSAML API call
// The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments.
extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion, SessionDuration)
}
}
// Called from 'onBeforeRequestEvent' function.
// Gets a Role Attribute from a SAMLAssertion as function argument. Gets the SAMLAssertion as a second argument.
// This function extracts the RoleArn and PrincipalArn (SAML-provider)
// from this argument and uses it to call the AWS STS assumeRoleWithSAML API.
function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion, SessionDuration) {
// Pattern for Role
var reRole = /arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i;
// Patern for Principal (SAML Provider)
var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i;
// Extraxt both regex patterns from SAMLAssertion attribute
RoleArn = samlattribute.match(reRole)[0];
PrincipalArn = samlattribute.match(rePrincipal)[0];
// Set parameters needed for assumeRoleWithSAML method
var params = {
PrincipalArn: PrincipalArn,
RoleArn: RoleArn,
SAMLAssertion: SAMLAssertion
};
if (SessionDuration !== null) {
params['DurationSeconds'] = SessionDuration;
}
// Call STS API from AWS
var sts = new AWS.STS();
sts.assumeRoleWithSAML(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else {
// On succesful API response create file with the STS keys
var docContent = "[default]" + LF +
"aws_access_key_id = " + data.Credentials.AccessKeyId + LF +
"aws_secret_access_key = " + data.Credentials.SecretAccessKey + LF +
"aws_session_token = " + data.Credentials.SessionToken;
// If there are no Role ARNs configured in the options panel, continue to create credentials file
// Otherwise, extend docContent with a profile for each specified ARN in the options panel
if (Object.keys(RoleArns).length == 0) {
console.log('Generate AWS tokens file.');
outputDocAsDownload(docContent);
} else {
var profileList = Object.keys(RoleArns);
console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]);
assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent, SessionDuration);
}
}
});
}
// Will fetch additional STS keys for 1 role from the RoleArns dict
// The assume-role API is called using the credentials (STS keys) fetched using the SAML claim. Basically the default profile.
function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, SessionToken, docContent, SessionDuration) {
// Set the fetched STS keys from the SAML reponse as credentials for doing the API call
var options = {'accessKeyId': AccessKeyId, 'secretAccessKey': SecretAccessKey, 'sessionToken': SessionToken};
var sts = new AWS.STS(options);
// Set the parameters for the AssumeRole API call. Meaning: What role to assume
var params = {
RoleArn: RoleArns[profileList[index]],
RoleSessionName: profileList[index]
};
if (SessionDuration !== null) {
params['DurationSeconds'] = SessionDuration;
}
// Call the API
sts.assumeRole(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else {
docContent += LF + LF +
"[" + profileList[index] + "]" + LF +
"aws_access_key_id = " + data.Credentials.AccessKeyId + LF +
"aws_secret_access_key = " + data.Credentials.SecretAccessKey + LF +
"aws_session_token = " + data.Credentials.SessionToken;
}
// If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile
// Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file
if (index < profileList.length - 1) {
console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[index + 1]]);
assumeAdditionalRole(profileList, index + 1, AccessKeyId, SecretAccessKey, SessionToken, docContent);
} else {
outputDocAsDownload(docContent);
}
});
}
// Called from either extractPrincipalPlusRoleAndAssumeRole (if RoleArns dict is empty)
// Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed
function outputDocAsDownload(docContent) {
var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) );
// Triggers download of the generated file
chrome.downloads.download({ url: doc, filename: FileName, conflictAction: 'overwrite', saveAs: false });
}
// This Listener receives messages from options.js and popup.js
// Received messages are meant to affect the background process.
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
// When the options are changed in the Options panel
// these items need to be reloaded in this background process.
if (request.action == "reloadStorageItems") {
loadItemsFromStorage();
sendResponse({message: "Storage items reloaded in background process."});
}
// When the activation checkbox on the popup screen is checked/unchecked
// the webRequest event listener needs to be added or removed.
if (request.action == "addWebRequestEventListener") {
addOnBeforeRequestEventListener();
sendResponse({message: "webRequest EventListener added in background process."});
}
if (request.action == "removeWebRequestEventListener") {
removeOnBeforeRequestEventListener();
sendResponse({message: "webRequest EventListener removed in background process."});
}
});
function loadItemsFromStorage() {
chrome.storage.sync.get({
FileName: 'credentials',
ApplySessionDuration: 'yes',
RoleArns: {}
}, function(items) {
FileName = items.FileName;
if (items.ApplySessionDuration == "no") {
ApplySessionDuration = false;
} else {
ApplySessionDuration = true;
}
RoleArns = items.RoleArns;
});
}