├── .npmignore ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | coverage 3 | examples 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | coverage* 4 | .tern-port 5 | v8.log 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awscred", 3 | "version": "1.5.0", 4 | "description": "Resolves AWS credentials (and region) using env, file and IAM strategies", 5 | "main": "index.js", 6 | "repository": "mhart/awscred", 7 | "keywords": [ 8 | "aws", 9 | "credentials", 10 | "region", 11 | "resolver", 12 | "resolve" 13 | ], 14 | "author": "Michael Hart ", 15 | "license": "MIT", 16 | "scripts": {} 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Michael Hart (michael.hart.au@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | awscred 2 | ------- 3 | 4 | A small standalone library to resolve AWS credentials and region details 5 | using, in order: environment variables, INI files, and HTTP calls (either to 6 | EC2 metadata or ECS endpoints, depending on environment). Queues HTTP calls to 7 | ensure no thundering herd effect will occur when credentials expire. 8 | 9 | Example 10 | ------- 11 | 12 | ```js 13 | var awscred = require('awscred') 14 | 15 | awscred.load(function(err, data) { 16 | if (err) throw err 17 | 18 | console.log(data.credentials) 19 | // { accessKeyId: 'ABC', 20 | // secretAccessKey: 'DEF', 21 | // sessionToken: 'GHI', 22 | // expiration: Sat Apr 25 2015 01:16:01 GMT+0000 (UTC) } 23 | 24 | console.log(data.region) 25 | // us-east-1 26 | }) 27 | ``` 28 | 29 | Or just load the credentials, if you know the region already: 30 | 31 | ```js 32 | awscred.loadCredentials(function(err, data) { 33 | if (err) throw err 34 | 35 | console.log(data) 36 | // { accessKeyId: 'ABC', 37 | // secretAccessKey: 'DEF', 38 | // sessionToken: 'GHI', 39 | // expiration: Sat Apr 25 2015 01:16:01 GMT+0000 (UTC) } 40 | }) 41 | ``` 42 | 43 | Or just load the region, synchronously: 44 | 45 | ```js 46 | console.log(awscred.loadRegionSync()) 47 | // us-east-1 48 | ``` 49 | 50 | 51 | 52 | API 53 | --- 54 | 55 | ### awscred.load([options], cb) 56 | ### awscred.loadCredentialsAndRegion([options], cb) 57 | 58 | Resolves AWS credentials and region details, and calls back with an object containing 59 | `credentials` and `region` properties as highlighted in the example above. 60 | 61 | `options` include: 62 | 63 | - `filename`: the name of the INI file to parse, defaults to `'~/.aws/credentials'` for credentials and `'~/.aws/config'` for region 64 | - `profile`: the name of the INI profile to use, defaults to `'default'` 65 | - `timeout`: the ms timeout on the http call to the EC2 or ECS metadata service, defaults to `5000` 66 | - `credentialsCallChain`: array of functions to resolve credentials, defaults to `awscred.credentialsCallChain` below 67 | - `regionCallChain`: array of functions to resolve region, defaults to `awscred.regionCallChain` below 68 | 69 | All options are also passed to `http.request`, so any standard Node.js HTTP 70 | options may be used as well. 71 | 72 | The following environment variables are checked by default: 73 | 74 | - `AWS_ACCESS_KEY_ID`, `AMAZON_ACCESS_KEY_ID`, `AWS_ACCESS_KEY` 75 | - `AWS_SECRET_ACCESS_KEY`, `AMAZON_SECRET_ACCESS_KEY`, `AWS_SECRET_KEY` 76 | - `AWS_SESSION_TOKEN`, `AMAZON_SESSION_TOKEN` 77 | - `AWS_REGION`, `AMAZON_REGION`, `AWS_DEFAULT_REGION` 78 | - `AWS_PROFILE`, `AMAZON_PROFILE` 79 | 80 | ### awscred.loadCredentials([options], cb) 81 | 82 | As above, but only resolves credentials, does not look up region. Calls 83 | back with just the credentials object (containing `accessKeyId`, 84 | `secretAccessKey`, and optionally `sessionToken` and `expiration` properties). 85 | 86 | ### awscred.loadRegion([options], cb) 87 | 88 | As above, but only resolves region, does not look up credentials. Calls 89 | back with just the region string. 90 | 91 | ### awscred.loadRegionSync([options]) 92 | 93 | As above, but returns the region directly from this function using synchronous calls. 94 | 95 | ### awscred.credentialsCallChain 96 | 97 | The array of credential loading functions used to determine call order. By default: 98 | `[loadCredentialsFromEnv, loadCredentialsFromIniFile, loadCredentialsFromHttp]` 99 | 100 | ### awscred.regionCallChain 101 | 102 | The array of region loading functions used to determine call order. By default: 103 | `[loadRegionFromEnv, loadRegionFromIniFile]` 104 | 105 | ### awscred.loadCredentialsFromEnv 106 | ### awscred.loadRegionFromEnv 107 | ### awscred.loadRegionFromEnvSync 108 | ### awscred.loadCredentialsFromIniFile 109 | ### awscred.loadRegionFromIniFile 110 | ### awscred.loadRegionFromIniFileSync 111 | ### awscred.loadCredentialsFromHttp 112 | ### awscred.loadCredentialsFromEc2Metadata 113 | ### awscred.loadCredentialsFromEcs 114 | ### awscred.loadProfileFromIniFile 115 | ### awscred.loadProfileFromIniFileSync 116 | 117 | Individual methods to load credentials and region from different sources. 118 | `loadCredentialsFromHttp` will choose between `loadCredentialsFromEc2Metadata` 119 | and `loadCredentialsFromEcs` depending on whether the 120 | `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` environment variable is set (as it is on ECS). 121 | 122 | ### awscred.merge(obj, [options], cb) 123 | 124 | Populates the `region` and `credentials` properties of `obj` using the 125 | appropriate `load` method – depending on whether they're already set or not. 126 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var http = require('http') 4 | var env = process.env 5 | 6 | var TIMEOUT_CODES = ['ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'Unknown system errno 64'] 7 | var httpCallbacks = [] 8 | 9 | exports.credentialsCallChain = [ 10 | loadCredentialsFromEnv, 11 | loadCredentialsFromIniFile, 12 | loadCredentialsFromHttp, 13 | ] 14 | 15 | exports.regionCallChain = [ 16 | loadRegionFromEnv, 17 | loadRegionFromIniFile, 18 | ] 19 | 20 | exports.load = exports.loadCredentialsAndRegion = loadCredentialsAndRegion 21 | exports.loadCredentials = loadCredentials 22 | exports.loadRegion = loadRegion 23 | exports.loadRegionSync = loadRegionSync 24 | exports.loadCredentialsFromEnv = loadCredentialsFromEnv 25 | exports.loadRegionFromEnv = loadRegionFromEnv 26 | exports.loadRegionFromEnvSync = loadRegionFromEnvSync 27 | exports.loadCredentialsFromIniFile = loadCredentialsFromIniFile 28 | exports.loadRegionFromIniFile = loadRegionFromIniFile 29 | exports.loadRegionFromIniFileSync = loadRegionFromIniFileSync 30 | exports.loadCredentialsFromHttp = loadCredentialsFromHttp 31 | exports.loadCredentialsFromEc2Metadata = loadCredentialsFromEc2Metadata 32 | exports.loadCredentialsFromEcs = loadCredentialsFromEcs 33 | exports.loadProfileFromIniFile = loadProfileFromIniFile 34 | exports.loadProfileFromIniFileSync = loadProfileFromIniFileSync 35 | exports.merge = merge 36 | 37 | function loadCredentialsAndRegion(options, cb) { 38 | if (!cb) { cb = options; options = {} } 39 | cb = once(cb) 40 | 41 | var out = {} 42 | var callsRemaining = 2 43 | 44 | function checkDone(propName) { 45 | return function(err, data) { 46 | if (err) return cb(err) 47 | out[propName] = data 48 | if (!--callsRemaining) return cb(null, out) 49 | } 50 | } 51 | 52 | loadCredentials(options, checkDone('credentials')) 53 | 54 | loadRegion(options, checkDone('region')) 55 | } 56 | 57 | function loadCredentials(options, cb) { 58 | if (!cb) { cb = options; options = {} } 59 | var credentialsCallChain = options.credentialsCallChain || exports.credentialsCallChain 60 | 61 | function nextCall(i) { 62 | credentialsCallChain[i](options, function(err, credentials) { 63 | if (err) return cb(err) 64 | 65 | if (credentials.accessKeyId && credentials.secretAccessKey) { 66 | return cb(null, credentials) 67 | } 68 | 69 | if (i >= credentialsCallChain.length - 1) { 70 | return cb(null, {}) 71 | } 72 | 73 | nextCall(i + 1) 74 | }) 75 | } 76 | nextCall(0) 77 | } 78 | 79 | function loadRegion(options, cb) { 80 | if (!cb) { cb = options; options = {} } 81 | var regionCallChain = options.regionCallChain || exports.regionCallChain 82 | 83 | function nextCall(i) { 84 | regionCallChain[i](options, function(err, region) { 85 | if (err) return cb(err) 86 | 87 | if (region) { 88 | return cb(null, region) 89 | } 90 | 91 | if (i >= regionCallChain.length - 1) { 92 | return cb(null, 'us-east-1') 93 | } 94 | 95 | nextCall(i + 1) 96 | }) 97 | } 98 | nextCall(0) 99 | } 100 | 101 | function loadRegionSync(options) { 102 | return loadRegionFromEnvSync() || loadRegionFromIniFileSync(options) 103 | } 104 | 105 | function loadCredentialsFromEnv(options, cb) { 106 | if (!cb) { cb = options; options = {} } 107 | 108 | cb(null, { 109 | accessKeyId: env.AWS_ACCESS_KEY_ID || env.AMAZON_ACCESS_KEY_ID || env.AWS_ACCESS_KEY, 110 | secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AMAZON_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY, 111 | sessionToken: env.AWS_SESSION_TOKEN || env.AMAZON_SESSION_TOKEN, 112 | }) 113 | } 114 | 115 | function loadRegionFromEnv(options, cb) { 116 | if (!cb) { cb = options; options = {} } 117 | 118 | cb(null, loadRegionFromEnvSync()) 119 | } 120 | 121 | function loadRegionFromEnvSync() { 122 | return env.AWS_REGION || env.AMAZON_REGION || env.AWS_DEFAULT_REGION 123 | } 124 | 125 | function loadCredentialsFromIniFile(options, cb) { 126 | if (!cb) { cb = options; options = {} } 127 | 128 | loadProfileFromIniFile(options, 'credentials', function(err, profile) { 129 | if (err) return cb(err) 130 | if (profile.aws_access_key_id) { 131 | return cb(null, { 132 | accessKeyId: profile.aws_access_key_id, 133 | secretAccessKey: profile.aws_secret_access_key, 134 | sessionToken: profile.aws_session_token, 135 | }) 136 | } 137 | loadProfileFromIniFile(options, 'config', function(err, profile) { 138 | if (err) return cb(err) 139 | cb(null, { 140 | accessKeyId: profile.aws_access_key_id, 141 | secretAccessKey: profile.aws_secret_access_key, 142 | sessionToken: profile.aws_session_token, 143 | }) 144 | }) 145 | }) 146 | } 147 | 148 | function loadRegionFromIniFile(options, cb) { 149 | if (!cb) { cb = options; options = {} } 150 | 151 | loadProfileFromIniFile(options, 'credentials', function(err, profile) { 152 | if (err) return cb(err) 153 | if (profile.region) return cb(null, profile.region) 154 | loadProfileFromIniFile(options, 'config', function(err, profile) { 155 | if (err) return cb(err) 156 | cb(null, profile.region) 157 | }) 158 | }) 159 | } 160 | 161 | function loadRegionFromIniFileSync(options) { 162 | return loadProfileFromIniFileSync(options || {}, 'credentials').region || 163 | loadProfileFromIniFileSync(options || {}, 'config').region 164 | } 165 | 166 | function loadCredentialsFromHttp(options, cb) { 167 | return process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ? 168 | loadCredentialsFromEcs(options, cb) : loadCredentialsFromEc2Metadata(options, cb) 169 | } 170 | 171 | function loadCredentialsFromEc2Metadata(options, cb) { 172 | if (!cb) { cb = options; options = {} } 173 | 174 | options.host = '169.254.169.254' 175 | options.resolvePath = function(options, cb) { 176 | options.path = '/latest/meta-data/iam/security-credentials/' 177 | 178 | request(options, function(err, res, data) { 179 | if (err) return cb(err) 180 | 181 | if (res.statusCode === 404) { 182 | return cb(new Error('Could not find IAM role. Check that you assigned an IAM role to your EC2 instance')) 183 | } 184 | 185 | if (res.statusCode !== 200) { 186 | return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data)) 187 | } 188 | 189 | cb(null, options.path + data.split('\n')[0]) 190 | }) 191 | } 192 | 193 | requestCredentials(options, cb) 194 | } 195 | 196 | function loadCredentialsFromEcs(options, cb) { 197 | if (!cb) { cb = options; options = {} } 198 | 199 | options.host = '169.254.170.2' 200 | options.resolvePath = function(options, cb) { 201 | cb(null, process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) 202 | } 203 | 204 | requestCredentials(options, cb) 205 | } 206 | 207 | function requestCredentials(options, cb) { 208 | httpCallbacks.push(cb) 209 | if (httpCallbacks.length > 1) return // only want one caller at a time 210 | 211 | cb = function(err, credentials) { 212 | httpCallbacks.forEach(function(cb) { cb(err, credentials) }) 213 | httpCallbacks = [] 214 | } 215 | 216 | if (options.timeout == null) options.timeout = 5000 217 | 218 | options.resolvePath(options, function(err, path) { 219 | if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {}) 220 | if (err) return cb(err) 221 | 222 | options.path = path 223 | 224 | request(options, function(err, res, data) { 225 | if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {}) 226 | if (err) return cb(err) 227 | 228 | if (res.statusCode !== 200) { 229 | return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data)) 230 | } 231 | 232 | try { data = JSON.parse(data) } catch (e) { } 233 | 234 | if (!data.AccessKeyId) { 235 | return cb(new Error('Failed to fetch IAM credentials: ' + JSON.stringify(data))) 236 | } 237 | 238 | cb(null, { 239 | accessKeyId: data.AccessKeyId, 240 | secretAccessKey: data.SecretAccessKey, 241 | sessionToken: data.Token, 242 | expiration: new Date(data.Expiration), 243 | }) 244 | }) 245 | }) 246 | } 247 | 248 | function loadProfileFromIniFile(options, defaultFilename, cb) { 249 | var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename) 250 | var profile = options.profile || resolveProfile() 251 | 252 | fs.readFile(filename, 'utf8', function(err, data) { 253 | if (err && err.code === 'ENOENT') return cb(null, {}) 254 | if (err) return cb(err) 255 | var parsedIni = parseAwsIni(data) 256 | cb(null, parsedIni['profile ' + profile] || parsedIni[profile] || {}) 257 | }) 258 | } 259 | 260 | function loadProfileFromIniFileSync(options, defaultFilename) { 261 | var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename) 262 | var profile = options.profile || resolveProfile() 263 | var data 264 | 265 | try { 266 | data = fs.readFileSync(filename, 'utf8') 267 | } catch (err) { 268 | if (err.code === 'ENOENT') return {} 269 | throw err 270 | } 271 | 272 | var parsedIni = parseAwsIni(data) 273 | return parsedIni['profile ' + profile] || parsedIni[profile] || {} 274 | } 275 | 276 | function merge(obj, options, cb) { 277 | if (!cb) { cb = options; options = {} } 278 | 279 | var needRegion = !obj.region 280 | var needCreds = !obj.credentials || !obj.credentials.accessKeyId || !obj.credentials.secretAccessKey 281 | 282 | function loadCreds(cb) { 283 | if (needRegion && needCreds) { 284 | return loadCredentialsAndRegion(options, cb) 285 | } else if (needRegion) { 286 | return loadRegion(options, function(err, region) { cb(err, { region: region }) }) 287 | } else if (needCreds) { 288 | return loadCredentials(options, function(err, credentials) { cb(err, { credentials: credentials }) }) 289 | } 290 | cb(null, {}) 291 | } 292 | 293 | loadCreds(function(err, creds) { 294 | if (err) return cb(err) 295 | 296 | if (creds.region) obj.region = creds.region 297 | if (creds.credentials) { 298 | if (!obj.credentials) { 299 | obj.credentials = creds.credentials 300 | } else { 301 | Object.keys(creds.credentials).forEach(function(key) { 302 | if (!obj.credentials[key]) obj.credentials[key] = creds.credentials[key] 303 | }) 304 | } 305 | } 306 | 307 | cb() 308 | }) 309 | } 310 | 311 | function resolveProfile() { 312 | return env.AWS_PROFILE || env.AMAZON_PROFILE || 'default' 313 | } 314 | 315 | function resolveHome() { 316 | return env.HOME || env.USERPROFILE || ((env.HOMEDRIVE || 'C:') + env.HOMEPATH) 317 | } 318 | 319 | // Fairly strict INI parser – will only deal with alpha keys, must be within sections 320 | function parseAwsIni(ini) { 321 | var section 322 | var out = Object.create(null) 323 | var re = /^\[([^\]]+)\]\s*$|^([a-z_]+)\s*=\s*(.+?)\s*$/ 324 | var lines = ini.split(/\r?\n/) 325 | 326 | lines.forEach(function(line) { 327 | var match = line.match(re) 328 | if (!match) return 329 | if (match[1]) { 330 | section = match[1] 331 | if (out[section] == null) out[section] = Object.create(null) 332 | } else if (section) { 333 | out[section][match[2]] = match[3] 334 | } 335 | }) 336 | 337 | return out 338 | } 339 | 340 | function request(options, cb) { 341 | cb = once(cb) 342 | 343 | var req = http.request(options, function(res) { 344 | var data = '' 345 | res.setEncoding('utf8') 346 | res.on('error', cb) 347 | res.on('data', function(chunk) { data += chunk }) 348 | res.on('end', function() { cb(null, res, data) }) 349 | }).on('error', cb) 350 | 351 | if (options.timeout != null) { 352 | req.setTimeout(options.timeout) 353 | req.on('timeout', function() { req.abort() }) 354 | } 355 | 356 | req.end() 357 | } 358 | 359 | function once(cb) { 360 | var called = false 361 | return function() { 362 | if (called) return 363 | called = true 364 | cb.apply(this, arguments) 365 | } 366 | } 367 | --------------------------------------------------------------------------------