├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── holochrome-1400x560.png ├── holochrome-440x280.png ├── holochrome-920x680.png ├── holochrome ├── disableReload.js ├── holochrome-128.png ├── inject.js ├── manifest.json └── script.js └── test ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.1.0" 4 | before_install: cd test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bridgewater Associates, LP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | | Browser | Version | Downloads | Rating | 4 | |---|---|---|---| 5 | |Chrome| [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/fgnplojdffjfbcmoldcfdoikldnogjpa.svg?maxAge=2592000?style=plastic)](https://chrome.google.com/webstore/detail/holochrome/fgnplojdffjfbcmoldcfdoikldnogjpa)|[![Chrome Web Store](https://img.shields.io/chrome-web-store/d/fgnplojdffjfbcmoldcfdoikldnogjpa.svg?maxAge=2592000?style=plastic)](https://chrome.google.com/webstore/detail/holochrome/fgnplojdffjfbcmoldcfdoikldnogjpa)|[![Chrome Web Store](https://img.shields.io/chrome-web-store/rating/fgnplojdffjfbcmoldcfdoikldnogjpa.svg?maxAge=2592000?style=plastic)](https://chrome.google.com/webstore/detail/holochrome/fgnplojdffjfbcmoldcfdoikldnogjpa)| 6 | |FireFox|[![Mozilla Add-on](https://img.shields.io/amo/v/bw-holochrome.svg)](https://addons.mozilla.org/en-US/firefox/addon/bw-holochrome/)|[![Mozilla Add-on](https://img.shields.io/amo/d/bw-holochrome.svg)](https://addons.mozilla.org/en-US/firefox/addon/bw-holochrome/)|[![Mozilla Add-on](https://img.shields.io/amo/rating/bw-holochrome.svg)](https://addons.mozilla.org/en-US/firefox/addon/bw-holochrome/)| 7 | 8 | Holochrome is a chrome extension that allows you to easily log in and switch between your AWS accounts using a single key stroke. It is built on top of the [aws instance metadata service](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) and therefore encourages security best practices by completely removing the need for static, long-lived credentials. The AWS console session is granted the exact same permissions as the [IAM role](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) available via the metadata service. 9 | 10 | # How do I get it? 11 | 12 | Chrome: [Chrome Web Store](https://chrome.google.com/webstore/detail/holochrome/fgnplojdffjfbcmoldcfdoikldnogjpa) 13 | 14 | Firefox: [Mozilla Add-on Store](https://addons.mozilla.org/en-US/firefox/addon/bw-holochrome) 15 | 16 | # How do I use it? 17 | 18 | Either click the icon or use the keyboard shortcut: 19 | 20 | Mac: `Cmd+Shift+1` 21 | 22 | Win: `Ctrl+Shift+1` 23 | 24 | The keyboard shortcut is a 'global' listener which will bring Chrome into focus once pressed. 25 | 26 | Chrome allows you to modify the keyboard shortcut on the chrome://extensions page. Check out [this guide](http://www.howtogeek.com/127162/how-to-create-custom-keyboard-shortcuts-for-browser-actions-and-extensions-in-google-chrome/) for more details. 27 | 28 | # How do I get the instance metadata service? 29 | 30 | If you run a machine in AWS with an IAM role, it will exist by default and things should run smoothly. 31 | 32 | If you want to leverage Holochrome on your local development machine, check out [AdRoll's Hologram](https://github.com/AdRoll/hologram). 33 | 34 | 35 | # Developing 36 | 37 | Follow the Chrome Development Guidelines for [loading an unpacked extension](https://developer.chrome.com/extensions/getstarted#unpacked). Target the inner holochrome/ folder. 38 | 39 | To run tests: 40 | 41 | ``` 42 | cd test && npm install && npm test 43 | ``` 44 | 45 | ## TODO 46 | 47 | 1. Rotate session seamlessly without being logged out 48 | 2. Potentially support the [default credential provider chain](http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/credentials.html) to allow the AWS console to more closely mimic other AWS services. 49 | -------------------------------------------------------------------------------- /holochrome-1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bridgewater/holochrome/954675ef46d945601907286348f823e395e20dcc/holochrome-1400x560.png -------------------------------------------------------------------------------- /holochrome-440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bridgewater/holochrome/954675ef46d945601907286348f823e395e20dcc/holochrome-440x280.png -------------------------------------------------------------------------------- /holochrome-920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bridgewater/holochrome/954675ef46d945601907286348f823e395e20dcc/holochrome-920x680.png -------------------------------------------------------------------------------- /holochrome/disableReload.js: -------------------------------------------------------------------------------- 1 | console.log('Holochrome is disabling the AWS Console force reload dialog'); 2 | AWSC.jQuery(AWSC).trigger("cancel-auth-change-detect"); -------------------------------------------------------------------------------- /holochrome/holochrome-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bridgewater/holochrome/954675ef46d945601907286348f823e395e20dcc/holochrome/holochrome-128.png -------------------------------------------------------------------------------- /holochrome/inject.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var s = document.createElement('script'); 3 | s.src = chrome.extension.getURL('disableReload.js'); 4 | s.onload = function() { 5 | this.parentNode.removeChild(this); 6 | }; 7 | (document.head || document.documentElement).appendChild(s); 8 | })() -------------------------------------------------------------------------------- /holochrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "web_accessible_resources": [ 3 | "disableReload.js" 4 | ], 5 | "commands": { 6 | "open-console": { 7 | "global": true, 8 | "suggested_key": { 9 | "default": "Ctrl+Shift+1", 10 | "mac": "Command+Shift+1" 11 | }, 12 | "description": "Open a tab to AWS console" 13 | } 14 | }, 15 | "browser_action": { 16 | "name": "Click to open AWS Console. Or press Cmd+Shift+1.", 17 | "default_icon": "holochrome-128.png" 18 | }, 19 | "description": "Use your IAM role (from instance metadata) to open the AWS console.", 20 | "content_scripts": [ 21 | { 22 | "matches": [ 23 | "https://*.console.aws.amazon.com/*" 24 | ], 25 | "js": [ 26 | "inject.js" 27 | ] 28 | } 29 | ], 30 | "icons": { 31 | "128": "holochrome-128.png" 32 | }, 33 | "background": { 34 | "scripts": [ 35 | "script.js" 36 | ] 37 | }, 38 | "version": "1.3", 39 | "manifest_version": 2, 40 | "permissions": [ 41 | "notifications", 42 | "webRequest", 43 | "https://signin.aws.amazon.com/federation", 44 | "https://*.signin.aws.amazon.com/oauth", 45 | "http://169.254.169.254/*", 46 | "https://console.aws.amazon.com/*", 47 | "https://aws.amazon.com/" 48 | ], 49 | "name": "Holochrome" 50 | } 51 | -------------------------------------------------------------------------------- /holochrome/script.js: -------------------------------------------------------------------------------- 1 | var federationUrlBase = "https://signin.aws.amazon.com/federation"; 2 | var awsConsole = 'https://console.aws.amazon.com/'; 3 | var logoutUrl = awsConsole + 'console/logout!doLogout'; 4 | 5 | var createNotification = function(message) { 6 | chrome.notifications.create({ 7 | type: "basic", 8 | title: 'Holochrome', 9 | message: message, 10 | iconUrl: 'holochrome-128.png' 11 | }); 12 | }; 13 | 14 | var request = function(url, callback, isEvent, attempts=0) { 15 | 16 | if (attempts > 2) { 17 | console.log("Too many attempts. Retrying from beginning."); 18 | getMyCreds(isEvent); 19 | return; 20 | } 21 | 22 | var xhr = new XMLHttpRequest(); 23 | xhr.open("GET", url, true); 24 | xhr.setRequestHeader("Content-Type", "application/json"); 25 | xhr.onreadystatechange = function() { 26 | if (xhr.readyState === 4) { 27 | switch (xhr.status) { 28 | case 200: 29 | callback(xhr.responseText); 30 | break; 31 | case 400: 32 | // follow logout redirect, then re-administer login command 33 | request(logoutUrl, function(){ 34 | request(url, callback, isEvent, attempts++); 35 | }, false, attempts++); 36 | break; 37 | case 0: 38 | var errorMessage = "The instance metadata service could not be reached."; 39 | console.log(errorMessage); 40 | if (isEvent) { 41 | createNotification(errorMessage); 42 | } 43 | break; 44 | case 500: 45 | var errorMessage = "Cannot find IAM role. Are you on a machine with an instance profile?"; 46 | console.log(errorMessage); 47 | if (isEvent) { 48 | createNotification(errorMessage); 49 | } 50 | break; 51 | default: 52 | break; 53 | } 54 | } 55 | }; 56 | console.log('Making HTTP request to: ' + url); 57 | xhr.send(); 58 | } 59 | 60 | var getSigninToken = function(creds, isEvent) { 61 | var signinTokenUrl = federationUrlBase 62 | + '?Action=getSigninToken' 63 | + '&Session=' + encodeURIComponent(JSON.stringify(creds)); 64 | var onComplete = function(response) { 65 | response = JSON.parse(response); 66 | getSessionCookies(response['SigninToken'], isEvent); 67 | }; 68 | request(signinTokenUrl, onComplete, isEvent); 69 | }; 70 | 71 | var getSessionCookies = function(signinToken, isEvent) { 72 | var federationUrl = federationUrlBase 73 | + '?Action=login' 74 | + '&Issuer=holochrome' 75 | + '&Destination=' + encodeURIComponent(awsConsole) 76 | + '&SigninToken=' + signinToken; 77 | var onComplete = function(response) { 78 | openTabWithConsole(isEvent); 79 | }; 80 | request(federationUrl, onComplete, isEvent); 81 | }; 82 | 83 | var getMyCreds = function(isEvent){ 84 | var metadataUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"; 85 | var onComplete = function(response) { 86 | profileName = response.split("\n")[0]; // a bit of a hack, but everyone does it :( 87 | getMyCredsFromProfile(isEvent, profileName); 88 | }; 89 | request(metadataUrl, onComplete, isEvent); 90 | } 91 | 92 | var getMyCredsFromProfile = function(isEvent, profileName){ 93 | var metadataUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/" + profileName; 94 | var onComplete = function(response) { 95 | response = JSON.parse(response); 96 | var myCreds = { 97 | 'sessionId': response["AccessKeyId"], 98 | 'sessionKey': response["SecretAccessKey"], 99 | 'sessionToken': response["Token"] 100 | }; 101 | getSigninToken(myCreds, isEvent); 102 | }; 103 | request(metadataUrl, onComplete, isEvent); 104 | } 105 | 106 | var openTabWithConsole = function(isEvent){ 107 | if(isEvent) { 108 | chrome.windows.getLastFocused(function(window){ 109 | chrome.windows.update(window.id, 110 | { 111 | focused: true 112 | }); 113 | chrome.tabs.create( 114 | { 115 | windowId: window.id, 116 | url: awsConsole, 117 | active: true 118 | }); 119 | }); 120 | } 121 | } 122 | 123 | var eventTriggered = function(arg) { 124 | console.log("Event received."); 125 | getMyCreds(true); 126 | }; 127 | 128 | chrome.commands.onCommand.addListener(eventTriggered); 129 | 130 | chrome.browserAction.onClicked.addListener(eventTriggered); 131 | 132 | var init = (function(){ 133 | getMyCreds(false); 134 | // tokens last 60 minutes, so we 135 | // refresh every 20 minutes to be safe 136 | setInterval(getMyCreds, 1200000, false); 137 | })(); 138 | 139 | 140 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "holochrome-tests", 3 | "version": "1.0.0", 4 | "description": "Holochrome test suite", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Bridgewater/holochrome" 12 | }, 13 | "keywords": [ 14 | "aws", 15 | "chrome", 16 | "extension", 17 | "login", 18 | "auth", 19 | "authorize", 20 | "authenticate" 21 | ], 22 | "author": "Bridgewater Associates OSS", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/Bridgewater/holochrome/issues" 26 | }, 27 | "homepage": "https://github.com/Bridgewater/holochrome", 28 | "devDependencies": { 29 | "expect": "^1.20.2", 30 | "mocha": "^3.1.0", 31 | "sinon": "^1.17.6", 32 | "sinon-chrome": "^1.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect'); 2 | var sinon = require('sinon'); 3 | 4 | describe('holochrome', function(){ 5 | beforeEach(() => { 6 | global.chrome = require('sinon-chrome'); 7 | createFakeXhr(); 8 | numCycles = 0; 9 | }); 10 | 11 | afterEach(() => { 12 | xhr.restore(); 13 | global.chrome.reset(); 14 | }); 15 | 16 | var createFakeXhr = function() { 17 | xhr = sinon.useFakeXMLHttpRequest(); 18 | global.XMLHttpRequest = xhr; 19 | requests = []; 20 | xhr.onCreate = (xhr) => { 21 | requests.push(xhr); 22 | }; 23 | }; 24 | 25 | var createCompleteXhrResponse = function(){ 26 | var totalRequiredRequests = 4; 27 | var numAttempts = numCycles * totalRequiredRequests; 28 | numCycles++; 29 | expect(requests.length).toBe(numAttempts+1); 30 | expect(requests[numAttempts+0].method).toBe('GET'); 31 | expect(requests[numAttempts+0].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); 32 | requests[numAttempts+0].respond(200, {}, 'iam-role-name\n'); 33 | 34 | expect(requests.length).toBe(numAttempts+2); 35 | expect(requests[numAttempts+1].method).toBe('GET'); 36 | expect(requests[numAttempts+1].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/iam-role-name'); 37 | requests[numAttempts+1].respond(200, {}, JSON.stringify({ 38 | 'AccessKeyId': 'omg', 39 | 'SecretAccessKey': 'such', 40 | 'Token': 'wow' 41 | })); 42 | 43 | expect(requests.length).toBe(numAttempts+3); 44 | expect(requests[numAttempts+2].method).toBe('GET'); 45 | expect(requests[numAttempts+2].url).toBe('https://signin.aws.amazon.com/federation?Action=getSigninToken&Session=%7B%22sessionId%22%3A%22omg%22%2C%22sessionKey%22%3A%22such%22%2C%22sessionToken%22%3A%22wow%22%7D'); 46 | requests[numAttempts+2].respond(200, {}, JSON.stringify({ 47 | 'SigninToken':'token' 48 | })); 49 | 50 | expect(requests.length).toBe(numAttempts+4); 51 | expect(requests[numAttempts+3].method).toBe('GET'); 52 | expect(requests[numAttempts+3].url).toBe('https://signin.aws.amazon.com/federation?Action=login&Issuer=holochrome&Destination=https%3A%2F%2Fconsole.aws.amazon.com%2F&SigninToken=token'); 53 | // TODO: set cookie? 54 | requests[numAttempts+3].respond(200, {}, ''); 55 | } 56 | 57 | var loadHolochrome = function(){ 58 | var scriptPath = '../holochrome/script.js' 59 | delete require.cache[require.resolve(scriptPath)]; 60 | chrome.commands.onCommand._listeners = [] 61 | chrome.browserAction.onClicked._listeners = [] 62 | holochrome = require(scriptPath); 63 | createCompleteXhrResponse(); 64 | return holochrome 65 | }; 66 | 67 | 68 | it("loads a cookie upon init", function(){ 69 | loadHolochrome(); 70 | }); 71 | 72 | it("doesn't open a tab upon init", function(){ 73 | loadHolochrome(); 74 | expect(chrome.tabs.create.called).toBeFalsy(); 75 | }); 76 | 77 | it("should attach the same event listener on init", function () { 78 | loadHolochrome(); 79 | expect(chrome.commands.onCommand.addListener.calledOnce).toBeTruthy(); 80 | expect(chrome.browserAction.onClicked.addListener.calledOnce).toBeTruthy(); 81 | expect(chrome.browserAction.onClicked._listeners).toEqual(chrome.commands.onCommand._listeners); 82 | }); 83 | 84 | it("brings chrome into focus on event", function(){ 85 | chrome.windows.getLastFocused.yields({ 86 | 'id': 1 87 | }); 88 | loadHolochrome(); 89 | chrome.commands.onCommand.trigger(); 90 | createCompleteXhrResponse(); 91 | expect(chrome.windows.update.withArgs(1, {focused:true}).calledOnce).toBeTruthy(); 92 | }); 93 | 94 | it("opens an active tab on event", function(){ 95 | chrome.windows.getLastFocused.yields({ 96 | 'id': 1 97 | }); 98 | loadHolochrome(); 99 | chrome.browserAction.onClicked.trigger(); 100 | createCompleteXhrResponse(); 101 | expect(chrome.tabs.create.withArgs({ 102 | windowId: 1, 103 | url: 'https://console.aws.amazon.com/', 104 | active: true 105 | }).calledOnce).toBeTruthy(); 106 | }); 107 | 108 | it("creates error notification if it can't find the metadata service", function(){ 109 | loadHolochrome(); 110 | chrome.browserAction.onClicked.trigger(); 111 | expect(requests[4].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); 112 | requests[4].respond(0, {}, ''); 113 | expect(chrome.notifications.create.calledOnce).toBeTruthy(); 114 | }); 115 | 116 | it("creates error notification if it can't find an IAM role", function(){ 117 | loadHolochrome(); 118 | chrome.browserAction.onClicked.trigger(); 119 | expect(requests[4].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); 120 | requests[4].respond(200, {}, 'non-existent-role\n'); 121 | expect(requests[5].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/non-existent-role'); 122 | requests[5].respond(500, {}, ''); 123 | expect(chrome.notifications.create.calledOnce).toBeTruthy(); 124 | }); 125 | 126 | it("doesn't display a notification if it fails to refresh in the background", function(){ 127 | global.clock = sinon.useFakeTimers(); 128 | loadHolochrome(); 129 | global.clock.tick(1200000); 130 | expect(requests[4].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); 131 | requests[4].respond(0, {}, ''); 132 | expect(chrome.notifications.create.notCalled).toBeTruthy(); 133 | global.clock = sinon.restore(); 134 | }); 135 | 136 | it("automatically logs out and back in when switching accounts", function(){ 137 | loadHolochrome(); 138 | chrome.commands.onCommand.trigger(); 139 | // this is basically just testing the logic of retrying the most recent 140 | // request when receiving a 400 141 | // i use the original request to the metadata service here to reduce testing complexity 142 | requests[4].respond(400, {}, ''); 143 | expect(requests[5].url).toBe('https://console.aws.amazon.com/console/logout!doLogout'); 144 | requests[5].respond(200, {}, ''); 145 | expect(requests[6].url).toBe('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); 146 | }); 147 | }); 148 | 149 | 150 | --------------------------------------------------------------------------------