├── README.md └── src ├── Detailed Description.txt ├── clock.png ├── eventPage.js ├── gear.png ├── manifest.json ├── notify_lock.png ├── notify_unlock.png ├── options.html ├── options.js ├── retry.png └── working.png /README.md: -------------------------------------------------------------------------------- 1 | # CASTER 2 | Chrome AWS SAML Token Expiry Reminder 3 | 4 | This Chrome extension: 5 | - automates the re-login of AWS SAML (eg ADFS) federated credentials 6 | - provides an indication of the time remaining until the current AWS credentials expire. 7 | 8 | It is published in the Chrome Web Store: 9 | - https://chrome.google.com/webstore/detail/chrome-aws-saml-token-exp/mbfkedefmlagincpblmgeeeehhamgpbn 10 | 11 | For more info see the [Detailed Description.txt](docs/Detailed Description.txt) file. 12 | -------------------------------------------------------------------------------- /src/Detailed Description.txt: -------------------------------------------------------------------------------- 1 | >>> UPDATE v1.0.11 2 | - Detect console page as success after posting for reauth (thanks walterking) 3 | 4 | >>> UPDATE v1.0.10 5 | - fixed a stupid bug from previous update which broke re-auth. Sorry. 6 | 7 | >>> UPDATE v1.0.9 8 | - fixed bug for authentication with no existing role/credentials 9 | - added an option to not show notification popup 10 | - published to Github (https://github.com/gitlon/CASTER) 11 | 12 | >>> WHY 13 | AWS ADFS Federation (SAML) tokens have a maximum expiry of 1 hour. 14 | 15 | Until Amazon change this limit, you are forced to re-login every 1 hour to use ADFS credentials for the AWS console and/or APIs. This is annoying. 16 | 17 | This CASTER extension automates the re-login, as well as providing an indication of the time remaining until the current AWS credentials expire. 18 | 19 | >>> HOW 20 | - look at cookies from the AWS Console websites 21 | - parse out the current user, role and expiry time 22 | - when nearly expired, try to post to the specified ADFS URL to regenerate a SAML token 23 | - then post again, to the common AWS SAML login page, to select the current AWS role 24 | - if successful, the result is another credentialled-login for another 1 hour. 25 | 26 | This only works if your credentials are saved - ie that you can navigate to your ADFS page and not be prompted for username and password after the first login. 27 | 28 | >>> YOU (THE USER) MUST DO THIS 29 | You MUST enter *your own* organisation's ADFS url, including the AWS role-provider. Eg: 30 | 31 | - https://YOURADFS.com/adfs/ls/idpinitiatedsignon?loginToRp=urn:amazon:webservices/ 32 | 33 | You should also review the default options. 34 | 35 | >>> OPTIONS 36 | - Attempt re-authentication as the current user/role? 37 | - Show a Chrome notification before credentials are nearly expired? 38 | - The URL for your organisation's ADFS endpoint 39 | - When (minutes before) to change the icon time text to yellow 40 | - When to change the icon time text to red 41 | - When to show the Chrome notification 42 | - When to attempt to re-authenticate 43 | 44 | >>> WHY THESE PERMISSIONS 45 | CASTER only needs permissions to two websites: your ADFS URL, and the standard AWS SAML role-choice page. Because you set your ADFS URL as an option, this can't be declared as a permission request. Therefore the extensions declares a request for "all websites". (This will be refined in a later version.) 46 | 47 | >>> OTHER DETAILS 48 | - What does CASTER mean? 49 | Chrome AWS SAML Token Expiry Reminder. 50 | 51 | - I don't want you to steal my AWS or company passwords!? 52 | CASTER doesn't store any passwords, or collect or send any private data. It gathers from readily-accessible cookies, and reposts using credentials saved by your browser - in the same way that a user would. You can easily review the extension code to confirm that this is the case. 53 | 54 | - Organization is spelled wrong in this text!? 55 | Not in Australia. 56 | 57 | - This extension looks like it was written by some newbie that only just learned about Chrome Extensions and Javascript!! 58 | Yes. 59 | 60 | - Feature XYZ could be done so much better!? 61 | Please contribute: https://github.com/gitlon/CASTER 62 | 63 | - Is this an official, supported product? 64 | CASTER is in no way related to, supported by or approved by Amazon, AWS or Amazon Web Services, or any other organisation. It was written by one individual for his own purposes, and is provided to the public without any warranty or guarantee that it is fit for any purpose. 65 | 66 | - How do we berate you? 67 | Send questions, comments or whisky to: caster [dot] extension [at] gmail.com. 68 | -------------------------------------------------------------------------------- /src/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlon/CASTER/257aa74a38a775f5c1c0d735ecebcec91e75fd51/src/clock.png -------------------------------------------------------------------------------- /src/eventPage.js: -------------------------------------------------------------------------------- 1 | 2 | var _expTime = new Date(0); 3 | var _msg = ''; 4 | var _mins = -1; 5 | var _rtab = null; 6 | var _relogrole = ''; 7 | var _rlistener; 8 | var _clistener; 9 | var _crescan; 10 | var _doDebug = true; 11 | var _prevInfo = {}; 12 | 13 | var _defaultprefs = { 14 | relogtime: 2, // minutes 15 | dorelogin: true, 16 | dopopup: true, 17 | loginurl: 'https://YOURADFS.com/adfs/ls/idpinitiatedsignon?loginToRp=urn:amazon:webservices/', 18 | alerttime: 5, // minutes 19 | warntime: 10, // minutes 20 | dangerzone: 5 // minutes 21 | }; 22 | 23 | var _prefs = {}; 24 | 25 | const ALARMNAME = 'casterAlarm'; 26 | const NOTIFYNAME = 'casterNotify'; 27 | const STOREKEY = 'caster_prefs'; 28 | 29 | if (!chrome.cookies) { 30 | chrome.cookies = chrome.experimental.cookies; 31 | } 32 | 33 | // Check whether new version is installed 34 | chrome.runtime.onInstalled.addListener(function(details){ 35 | writeLog("Installed"); 36 | if(details.reason == "install") { 37 | writeLog("This is a first install!"); 38 | chrome.runtime.openOptionsPage(); 39 | } else if (details.reason == "update") { 40 | var thisVersion = chrome.runtime.getManifest().version; 41 | writeLog("Updated from " + details.previousVersion + " to " + thisVersion + "!"); 42 | } 43 | }); 44 | 45 | chrome.cookies.onChanged.addListener(cookieListener); 46 | chrome.alarms.onAlarm.addListener(alarmHandling); 47 | chrome.notifications.onButtonClicked.addListener(notificationHandling); 48 | chrome.browserAction.onClicked.addListener(popupClickHandling); 49 | chrome.runtime.onMessage.addListener(optionQueryHandling); 50 | chrome.omnibox.onInputStarted.addListener(onInputStartedHandling); 51 | chrome.omnibox.onInputChanged.addListener(onInputChangedHandling); 52 | chrome.omnibox.onInputEntered.addListener(onInputEnteredHandling); 53 | chrome.tabs.onUpdated.addListener(doPostBack); 54 | 55 | function writeLog() { 56 | if (_doDebug) { 57 | var a = []; 58 | for (i = 0; i < arguments.length; i++) { 59 | a.push(arguments[i]); 60 | } 61 | console.log.apply(console, a); // write out all arguments as an array 62 | } 63 | } 64 | 65 | function onInputStartedHandling() { 66 | chrome.omnibox.setDefaultSuggestion({description: 'Press enter to action'}); 67 | } 68 | 69 | function onInputChangedHandling(typed, reply) { 70 | var r = []; 71 | switch (true) { 72 | case ('reauth'.startsWith(typed) && typed.length > 0): { 73 | r.push({content: 'reauth', description: '[reauth] try to login again with the same user and role'}); 74 | break; 75 | } 76 | case ('options'.startsWith(typed) && typed.length > 0): { 77 | r.push({content: 'options', description: '[options] show the options page'}); 78 | break; 79 | } 80 | case (typed == ' ' || typed.length == 0): { 81 | r.push({content: ' ', description: '[blank] show the details notification'}); 82 | break; 83 | } 84 | default: { break; } 85 | } 86 | reply(r); 87 | } 88 | 89 | function onInputEnteredHandling(typed, dispo) { 90 | writeLog('onInputEnteredHandling', typed); 91 | switch (true) { 92 | case (typed == ' ' || typed.length == 0): { popupClickHandling(dispo); break; } 93 | case (typed == 'reauth'): { doReLogin(); break; } 94 | case (typed == 'options'): { chrome.runtime.openOptionsPage(); break; } 95 | default: { break; } 96 | } 97 | } 98 | 99 | 100 | function optionQueryHandling(msg, from, callback) { 101 | writeLog('optionQueryHandling', msg, from); 102 | var a = {answer: ''}; 103 | switch (true) { 104 | case (msg.q == 'defaults'): { a.answer = _defaultprefs; break; } 105 | case (msg.q == 'save'): { 106 | _prefs = msg.p; 107 | setUserPrefs(); 108 | alarmHandling('from queryHandling'); 109 | a.answer = 'ok'; 110 | break; 111 | } 112 | case (msg.q == 'get'): { a.answer = _prefs; break; } 113 | default: { break; } 114 | } 115 | writeLog(a); 116 | callback(a); 117 | }; 118 | 119 | function getUserPrefs() { 120 | writeLog('getUserPrefs'); 121 | chrome.storage.sync.get(STOREKEY, function(items) { 122 | if (items[STOREKEY]) { 123 | writeLog('got from storage'); 124 | _prefs = items[STOREKEY]; 125 | if (!("dopopup" in _prefs)) { // 1.0.8 to 1.0.9 migration 126 | writeLog('prefs migration'); 127 | _prefs.dopopup = _defaultprefs.dopoup; 128 | } 129 | writeLog(_prefs); 130 | } else { 131 | writeLog('got from defaults'); 132 | _prefs = _defaultprefs; 133 | setUserPrefs(); 134 | evalExpiryTime(); 135 | } 136 | }); 137 | } 138 | 139 | function setUserPrefs() { 140 | writeLog('setUserPrefs'); 141 | var s = {'caster_prefs': _prefs}; // cant use STOREKEY here 142 | chrome.storage.sync.set(s); 143 | } 144 | 145 | function doReLogin() { 146 | writeLog('doReLogin'); 147 | if (_rtab == null) { 148 | 149 | var p = { 150 | type: 'progress', 151 | title: 'AWS Credential Expiry', 152 | message: 'Attempting relogin\nfor: ' + _prevInfo.user + ' as ' + _prevInfo.role + '\nvia: ' + _prefs.loginurl, 153 | isClickable: false, 154 | priority: 0, 155 | progress: 50, 156 | iconUrl: 'working.png' 157 | }; 158 | chrome.notifications.clear(NOTIFYNAME); 159 | 160 | if (!(_prefs.dorelogin)) { 161 | writeLog('skipping relogin per user pref'); 162 | p.message = 'skipping relogin per user preferences'; 163 | p.progress = 0; 164 | } 165 | 166 | if (_prefs.dopopup) { 167 | chrome.notifications.create(NOTIFYNAME, p); 168 | } else { 169 | writeLog('skipping notification from doReLogin'); 170 | } 171 | 172 | if (_prefs.dorelogin) { 173 | writeLog('trying to create login tab'); 174 | _clistener = false; 175 | _rlistener = true; 176 | chrome.tabs.create({url: _prefs.loginurl, active: false}, function (t) { 177 | _rtab = t; 178 | }); 179 | } 180 | } else { 181 | writeLog('reLogin already in progress'); 182 | } 183 | } 184 | 185 | function doPostBack(tabid, change, tab) { 186 | writeLog(tabid, change, tab); 187 | if (_rlistener) { 188 | if (_rtab.id == tabid) { 189 | if (change.status == 'complete' && (tab.url == 'https://signin.aws.amazon.com/saml' || tab.url.indexOf('console.aws.amazon.com') > 0) && (!(_relogrole))) { 190 | writeLog('doPostBack ready'); 191 | if (!_prevInfo.userInfo) { 192 | writeLog('need to invoke evalAllCookies later'); 193 | } else { 194 | _relogaccountid = _prevInfo.userInfo.arn.split(":")[4] 195 | _relogrole = _prevInfo.userInfo.arn.split(":")[5].split("/")[1] 196 | writeLog('_relogaccountid', _relogaccountid); 197 | writeLog('_relogrole', _relogrole); 198 | chrome.tabs.executeScript(tabid, 199 | { 200 | code: 'var input = document.getElementsByTagName("input");' + 201 | 'for (i = 0; i < input.length; i++) {' + 202 | ' var el = input[i];' + 203 | ' console.log(el.id);' + 204 | ' if (' + 205 | ' el.type == "radio" &&' + 206 | ' el.id.indexOf("' + _relogaccountid + '") > -1 &&' + 207 | ' el.id.indexOf("' + _relogrole + '") > -1' + 208 | ' ) {' + 209 | ' el.checked = true;' + 210 | ' document.getElementById("saml_form").submit();' + 211 | ' break;' + 212 | ' }' + 213 | '}' 214 | }, function (n) { 215 | writeLog('posted'); 216 | } 217 | ); 218 | } 219 | } 220 | else if (change.status == 'complete') { 221 | if (_relogrole) { 222 | writeLog('doPostBack done'); 223 | _rlistener = false; 224 | _rtab = null; 225 | evalAllCookies(); 226 | try { 227 | chrome.tabs.remove(tabid); 228 | } catch (err) { 229 | writeLog(err); 230 | } 231 | _clistener = true; 232 | } else { 233 | writeLog('invoking evalAllCookies'); 234 | evalAllCookies(); 235 | } 236 | } 237 | } 238 | } 239 | } 240 | 241 | function notificationHandling(id, button) { 242 | writeLog('notificationHandling', id, button); 243 | switch (true) { 244 | case (button == 0): { // Options 245 | chrome.runtime.openOptionsPage(); 246 | break; 247 | } 248 | case (button == 1): { // Authenticate 249 | doReLogin(); 250 | break; 251 | } 252 | default: { break; } 253 | } 254 | } 255 | 256 | function popupClickHandling(tab) { 257 | writeLog('popupClickHandling'); 258 | chrome.notifications.clear(NOTIFYNAME); 259 | evalExpiryTime(); 260 | writeLog('popupClickHandling calls doNotify with force'); 261 | doNotify(true); 262 | doUserUpdates(); 263 | } 264 | 265 | function alarmHandling(alarm) { 266 | writeLog('alarmHandling', alarm); 267 | evalExpiryTime(); 268 | doUserUpdates(); 269 | } 270 | 271 | function doNotify(force) { 272 | var mustDo = false; 273 | if (force === undefined) { 274 | writeLog('force was undefined'); 275 | // not passed 276 | } else { 277 | mustDo = force; 278 | writeLog('force was defined', force, mustDo); 279 | } 280 | 281 | if (_prefs.dopopup || mustDo) { 282 | writeLog('doNotify'); 283 | var p = { 284 | type: 'progress', 285 | title: 'AWS Credential Expiry', 286 | message: _msg.replace('AWS SAML Credentials\n', ''), 287 | isClickable: true, 288 | buttons: [ 289 | { title: 'Options', iconUrl: 'gear.png'}, 290 | { title: 'Authenticate', iconUrl: 'retry.png'} 291 | ], 292 | priority: 0 293 | } 294 | 295 | if (_mins > 0) { 296 | p.iconUrl = 'notify_lock.png'; 297 | p.progress = Math.round(100 * _mins / 60); 298 | } else { 299 | p.progress = 0; 300 | p.iconUrl = 'notify_unlock.png'; 301 | } 302 | 303 | chrome.notifications.create(NOTIFYNAME, p); 304 | } 305 | } 306 | 307 | function evalExpiryTime() { 308 | writeLog('evalExpiryTime'); 309 | var d = new Date(); 310 | _mins = Math.round((_expTime - d) / 1000 / 60); 311 | if (!(_expTime)) {_mins = -1; } 312 | if (_mins <= 0) { 313 | writeLog('not logged in any more'); 314 | _msg = 'AWS credentials expired'; 315 | if (_prevInfo.role) { _msg += '\npreviously: ' + _prevInfo.user + ' as ' + _prevInfo.role ; } 316 | } else { 317 | _msg = 'AWS SAML Credentials'; 318 | if (_prevInfo.role) { _msg += '\ncurrently: ' + _prevInfo.user + ' as ' + _prevInfo.role ; } 319 | _msg += '\nexpires: ' + _mins.toString() + ' min'; 320 | if (_mins > 1) { _msg += 's'; } 321 | } 322 | writeLog(_msg); 323 | } 324 | 325 | function doUserUpdates() { 326 | writeLog('doUserUpdates'); 327 | var c = '#008000'; 328 | 329 | switch (true) { 330 | case (_mins < _prefs.dangerzone): { c = '#FF0000'; break; } 331 | case (_mins < _prefs.warntime): { c = '#FFD700'; break; } 332 | case (_mins < 0): { c = '#000000'; break; } 333 | default: { break; } 334 | } 335 | 336 | chrome.browserAction.setTitle({title: _msg }); 337 | var m = '-'; 338 | if (_mins > 0) { m = _mins.toString(); } 339 | chrome.browserAction.setBadgeBackgroundColor({color: c}); 340 | chrome.browserAction.setBadgeText({text: m}); 341 | 342 | switch (true) { 343 | case (_mins <= 0): { 344 | chrome.notifications.clear(NOTIFYNAME); 345 | chrome.alarms.clear(ALARMNAME); 346 | writeLog('doUserUpdates calls doNotify without force to alert that time has expired'); 347 | doNotify(); 348 | break; 349 | } 350 | case (_mins <= _prefs.relogtime): { 351 | doReLogin(); 352 | break; 353 | } 354 | case (_mins <= _prefs.alerttime): { 355 | writeLog('doUserUpdates calls doNotify without force to alert based on user prefs'); 356 | doNotify(); 357 | break; 358 | } 359 | default: { 360 | break; } 361 | } 362 | } 363 | 364 | function evalThisCookie(c) { 365 | writeLog(c.name); 366 | if ((c.name == 'seance') && c.domain.endsWith('amazon.com')) { 367 | var t = new Date(0); 368 | t.setUTCSeconds((JSON.parse(decodeURIComponent(c.value))).exp / 1000); 369 | if (t > Date.now()) { 370 | if (t > _expTime) { 371 | _expTime = t; 372 | writeLog(c.domain + ', ' + c.name + ', ' + _expTime); 373 | chrome.alarms.create(ALARMNAME, {periodInMinutes:1}); 374 | if (_relogrole) { 375 | _relogrole = null; 376 | popupClickHandling(null); 377 | } 378 | } 379 | } 380 | } else if ((c.name == 'aws-userInfo') && c.domain.endsWith('amazon.com')) { 381 | var oldInfo = _prevInfo.userInfo; 382 | _prevInfo.userInfo = JSON.parse(decodeURIComponent(c.value)); 383 | var rr = _prevInfo.userInfo.username.replace('assumed-role/', '').split('/'); 384 | _prevInfo.role = rr[0]; 385 | _prevInfo.user = rr[1].split('@')[0]; 386 | writeLog(_prevInfo.userInfo, _prevInfo.role, _prevInfo.user); 387 | if (!(oldInfo)) { 388 | doUserUpdates(); 389 | } 390 | } 391 | } 392 | 393 | function cookieListener(info) { 394 | if (_clistener) { 395 | if (info.cookie.domain.endsWith('amazon.com') && info.removed) { 396 | writeLog('AWS cookie removed, need to re-evaluate'); 397 | evalAllCookies(); 398 | } else { 399 | evalThisCookie(info.cookie); 400 | } 401 | } 402 | } 403 | 404 | function evalAllCookies() { 405 | if (_crescan) { 406 | _crescan = false; 407 | writeLog('evalAllCookies'); 408 | chrome.cookies.getAll({}, function(c) { 409 | for (var i in c) { 410 | evalThisCookie(c[i]); 411 | } 412 | alarmHandling('from AllCookies'); 413 | _crescan = true; 414 | }); 415 | } 416 | } 417 | 418 | function onLoad() { 419 | getUserPrefs(); 420 | _crescan = true; 421 | evalAllCookies(); 422 | _clistener = true; 423 | } 424 | 425 | document.addEventListener('DOMContentLoaded', onLoad); 426 | -------------------------------------------------------------------------------- /src/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlon/CASTER/257aa74a38a775f5c1c0d735ecebcec91e75fd51/src/gear.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version" : 2, 3 | "name" : "Chrome AWS SAML Token Expiry Reminder", 4 | "version" : "1.0.12", 5 | "description" : "Re-authenticate with AWS federated ADFS (SAML) credentials when they expire.", 6 | "author" : "gitlon", 7 | "permissions" : ["cookies", "tabs", "https://*/adfs/ls/*", "https://*.aws.amazon.com/*", "https://*.amazon.com/*", "alarms", "notifications", "storage"], 8 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 9 | "icons" : { 10 | "16" : "clock.png", 11 | "48" : "clock.png", 12 | "128" : "clock.png" 13 | }, 14 | "browser_action" : { 15 | "default_icon" : "clock.png", 16 | "default_title" : "AWS SAML Credentials\nShows minutes until expiry" 17 | }, 18 | "options_ui" : { 19 | "page" : "options.html", 20 | "chrome_style" : true 21 | }, 22 | "background" : { 23 | "persistent" : false, 24 | "scripts" : ["eventPage.js"] 25 | }, 26 | "omnibox" : { 27 | "keyword" : "caster" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/notify_lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlon/CASTER/257aa74a38a775f5c1c0d735ecebcec91e75fd51/src/notify_lock.png -------------------------------------------------------------------------------- /src/notify_unlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitlon/CASTER/257aa74a38a775f5c1c0d735ecebcec91e75fd51/src/notify_unlock.png -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |Attempt re-login? | |
Show notifications? | |
ADFS URL | |
Before expiry (mins): | |
Warning yellow icon | |
Danger red icon | |
Notification popup | |
Attempt re-login |