function RequestSigner(request, credentials) { if (typeof request === 'string') request = url.parse(request) var headers = request.headers = (request.headers || {}), hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host) this.request = request this.credentials = credentials || this.defaultCredentials() this.service = request.service || hostParts[0] || '' this.region = request.region || hostParts[1] || 'us-east-1' // SES uses a different domain from the service name if (this.service === 'email') this.service = 'ses' if (!request.method && request.body) request.method = 'POST' if (!headers.Host && !headers.host) { headers.Host = request.hostname || request.host || this.createHost() // If a port is specified explicitly, use it as is if (request.port) headers.Host += ':' + request.port } if (!request.hostname && !request.host) request.hostname = headers.Host || headers.host this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT' }
...
}
}))
/*
(HTTP 202, empty response)
*/
// Generate CodeCommit Git access password
var signer = new aws4.RequestSigner({
service: 'codecommit',
host: 'git-codecommit.us-east-1.amazonaws.com',
method: 'GIT',
path: '/v1/repos/MyAwesomeRepo',
})
var password = signer.getDateTime() + 'Z' + signer.signature()
```
...
sign = function (request, credentials) { return new RequestSigner(request, credentials).sign() }
...
// alternatively (as aws4 can infer the host):
opts = {service: 'sqs', region: 'us-east-1', path: '/?Action=ListQueues'}
// alternatively (as us-east-1 is default):
opts = {service: 'sqs', path: '/?Action=ListQueues'}
aws4.sign(opts) // assumes AWS credentials are available in process.env
console.log(opts)
/*
{
host: 'sqs.us-east-1.amazonaws.com',
path: '/?Action=ListQueues',
headers: {
...
function RequestSigner(request, credentials) { if (typeof request === 'string') request = url.parse(request) var headers = request.headers = (request.headers || {}), hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host) this.request = request this.credentials = credentials || this.defaultCredentials() this.service = request.service || hostParts[0] || '' this.region = request.region || hostParts[1] || 'us-east-1' // SES uses a different domain from the service name if (this.service === 'email') this.service = 'ses' if (!request.method && request.body) request.method = 'POST' if (!headers.Host && !headers.host) { headers.Host = request.hostname || request.host || this.createHost() // If a port is specified explicitly, use it as is if (request.port) headers.Host += ':' + request.port } if (!request.hostname && !request.host) request.hostname = headers.Host || headers.host this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT' }
...
}
}))
/*
(HTTP 202, empty response)
*/
// Generate CodeCommit Git access password
var signer = new aws4.RequestSigner({
service: 'codecommit',
host: 'git-codecommit.us-east-1.amazonaws.com',
method: 'GIT',
path: '/v1/repos/MyAwesomeRepo',
})
var password = signer.getDateTime() + 'Z' + signer.signature()
```
...
authHeader = function () { return [ 'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(), 'SignedHeaders=' + this.signedHeaders(), 'Signature=' + this.signature(), ].join(', ') }
...
RequestSigner.prototype.sign = function() {
if (!this.parsedPath) this.prepareRequest()
if (this.request.signQuery) {
this.parsedPath.query['X-Amz-Signature'] = this.signature()
} else {
this.request.headers.Authorization = this.authHeader()
}
this.request.path = this.formatPath()
return this.request
}
...
canonicalHeaders = function () { var headers = this.request.headers function trimAll(header) { return header.toString().trim().replace(/\s+/g, ' ') } return Object.keys(headers) .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 }) .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) }) .join('\n') }
...
if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/')
}
return [
this.request.method || 'GET',
pathStr,
queryStr,
this.canonicalHeaders() + '\n',
this.signedHeaders(),
bodyHash,
].join('\n')
}
RequestSigner.prototype.canonicalHeaders = function() {
var headers = this.request.headers
...
canonicalString = function () { if (!this.parsedPath) this.prepareRequest() var pathStr = this.parsedPath.path, query = this.parsedPath.query, headers = this.request.headers, queryStr = '', normalizePath = this.service !== 's3', decodePath = this.service === 's3' || this.request.doNotEncodePath, decodeSlashesInPath = this.service === 's3', firstValOnly = this.service === 's3', bodyHash if (this.service === 's3' && this.request.signQuery) { bodyHash = 'UNSIGNED-PAYLOAD' } else if (this.isCodeCommitGit) { bodyHash = '' } else { bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] || hash(this.request.body || '', 'hex') } if (query) { queryStr = encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce(function(obj, key) { if (!key) return obj obj[key] = !Array.isArray(query[key]) ? query[key] : (firstValOnly ? query[key][0] : query[key].slice().sort()) return obj }, {}))) } if (pathStr !== '/') { if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/') pathStr = pathStr.split('/').reduce(function(path, piece) { if (normalizePath && piece === '..') { path.pop() } else if (!normalizePath || piece !== '.') { if (decodePath) piece = querystring.unescape(piece) path.push(encodeRfc3986(querystring.escape(piece))) } return path }, []).join('/') if (pathStr[0] !== '/') pathStr = '/' + pathStr if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/') } return [ this.request.method || 'GET', pathStr, queryStr, this.canonicalHeaders() + '\n', this.signedHeaders(), bodyHash, ].join('\n') }
...
}
RequestSigner.prototype.stringToSign = function() {
return [
'AWS4-HMAC-SHA256',
this.getDateTime(),
this.credentialString(),
hash(this.canonicalString(), 'hex'),
].join('\n')
}
RequestSigner.prototype.canonicalString = function() {
if (!this.parsedPath) this.prepareRequest()
var pathStr = this.parsedPath.path,
...
createHost = function () { var region = this.isSingleRegion() ? '' : (this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region, service = this.service === 'ses' ? 'email' : this.service return service + region + '.amazonaws.com' }
...
// SES uses a different domain from the service name
if (this.service === 'email') this.service = 'ses'
if (!request.method && request.body)
request.method = 'POST'
if (!headers.Host && !headers.host) {
headers.Host = request.hostname || request.host || this.createHost()
// If a port is specified explicitly, use it as is
if (request.port)
headers.Host += ':' + request.port
}
if (!request.hostname && !request.host)
request.hostname = headers.Host || headers.host
...
credentialString = function () { return [ this.getDate(), this.region, this.service, 'aws4_request', ].join('/') }
...
if (query['X-Amz-Date'])
this.datetime = query['X-Amz-Date']
else
query['X-Amz-Date'] = this.getDateTime()
query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString
()
query['X-Amz-SignedHeaders'] = this.signedHeaders()
} else {
if (!request.doNotModifyHeaders && !this.isCodeCommitGit) {
if (request.body && !headers['Content-Type'] && !headers['content-type'])
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
...
defaultCredentials = function () { var env = process.env return { accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY, secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY, sessionToken: env.AWS_SESSION_TOKEN, } }
...
if (typeof request === 'string') request = url.parse(request)
var headers = request.headers = (request.headers || {}),
hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host)
this.request = request
this.credentials = credentials || this.defaultCredentials()
this.service = request.service || hostParts[0] || ''
this.region = request.region || hostParts[1] || 'us-east-1'
// SES uses a different domain from the service name
if (this.service === 'email') this.service = 'ses'
...
formatPath = function () { var path = this.parsedPath.path, query = this.parsedPath.query if (!query) return path // Services don't support empty query string keys if (query[''] != null) delete query[''] return path + '?' + encodeRfc3986(querystring.stringify(query)) }
...
if (this.request.signQuery) {
this.parsedPath.query['X-Amz-Signature'] = this.signature()
} else {
this.request.headers.Authorization = this.authHeader()
}
this.request.path = this.formatPath()
return this.request
}
RequestSigner.prototype.getDateTime = function() {
if (!this.datetime) {
var headers = this.request.headers,
...
getDate = function () { return this.getDateTime().substr(0, 8) }
...
'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(),
'SignedHeaders=' + this.signedHeaders(),
'Signature=' + this.signature(),
].join(', ')
}
RequestSigner.prototype.signature = function() {
var date = this.getDate(),
cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(),
kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey)
if (!kCredentials) {
kDate = hmac('AWS4' + this.credentials.secretAccessKey, date)
kRegion = hmac(kDate, this.region)
kService = hmac(kRegion, this.service)
kCredentials = hmac(kService, 'aws4_request')
...
getDateTime = function () { if (!this.datetime) { var headers = this.request.headers, date = new Date(headers.Date || headers.date || new Date) this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '') // Remove the trailing 'Z' on the timestamp string for CodeCommit git access if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1) } return this.datetime }
...
// Generate CodeCommit Git access password
var signer = new aws4.RequestSigner({
service: 'codecommit',
host: 'git-codecommit.us-east-1.amazonaws.com',
method: 'GIT',
path: '/v1/repos/MyAwesomeRepo',
})
var password = signer.getDateTime() + 'Z' + signer.signature()
```
API
---
### aws4.sign(requestOptions, [credentials])
...
isSingleRegion = function () { // Special case for S3 and SimpleDB in us-east-1 if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts'] .indexOf(this.service) >= 0 }
...
if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return
true
return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts']
.indexOf(this.service) >= 0
}
RequestSigner.prototype.createHost = function() {
var region = this.isSingleRegion() ? '' :
(this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this
.region,
service = this.service === 'ses' ? 'email' : this.service
return service + region + '.amazonaws.com'
}
RequestSigner.prototype.prepareRequest = function() {
this.parsePath()
...
matchHost = function (host) { var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com$/) var hostParts = (match || []).slice(1, 3) // ES's hostParts are sometimes the other way round, if the value that is expected // to be region equals ‘es’ switch them back // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com if (hostParts[1] === 'es') hostParts = hostParts.reverse() return hostParts }
...
// request: { path | body, [host], [method], [headers], [service], [region] }
// credentials: { accessKeyId, secretAccessKey, [sessionToken] }
function RequestSigner(request, credentials) {
if (typeof request === 'string') request = url.parse(request)
var headers = request.headers = (request.headers || {}),
hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers
.host)
this.request = request
this.credentials = credentials || this.defaultCredentials()
this.service = request.service || hostParts[0] || ''
this.region = request.region || hostParts[1] || 'us-east-1'
...
parsePath = function () { var path = this.request.path || '/', queryIx = path.indexOf('?'), query = null if (queryIx >= 0) { query = querystring.parse(path.slice(queryIx + 1)) path = path.slice(0, queryIx) } // S3 doesn't always encode characters > 127 correctly and // all services don't encode characters > 255 correctly // So if there are non-reserved chars (and it's not already all % encoded), just encode them all if (/[^0-9A-Za-z!'()*\-._~%/]/.test(path)) { path = path.split('/').map(function(piece) { return querystring.escape(querystring.unescape(piece)) }).join('/') } this.parsedPath = { path: path, query: query, } }
...
var region = this.isSingleRegion() ? '' :
(this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.
region,
service = this.service === 'ses' ? 'email' : this.service
return service + region + '.amazonaws.com'
}
RequestSigner.prototype.prepareRequest = function() {
this.parsePath()
var request = this.request, headers = request.headers, query
if (request.signQuery) {
this.parsedPath.query = query = this.parsedPath.query || {}
...
prepareRequest = function () { this.parsePath() var request = this.request, headers = request.headers, query if (request.signQuery) { this.parsedPath.query = query = this.parsedPath.query || {} if (this.credentials.sessionToken) query['X-Amz-Security-Token'] = this.credentials.sessionToken if (this.service === 's3' && !query['X-Amz-Expires']) query['X-Amz-Expires'] = 86400 if (query['X-Amz-Date']) this.datetime = query['X-Amz-Date'] else query['X-Amz-Date'] = this.getDateTime() query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256' query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString() query['X-Amz-SignedHeaders'] = this.signedHeaders() } else { if (!request.doNotModifyHeaders && !this.isCodeCommitGit) { if (request.body && !headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8' if (request.body && !headers['Content-Length'] && !headers['content-length']) headers['Content-Length'] = Buffer.byteLength(request.body) if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token']) headers['X-Amz-Security-Token'] = this.credentials.sessionToken if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256']) headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex') if (headers['X-Amz-Date'] || headers['x-amz-date']) this.datetime = headers['X-Amz-Date'] || headers['x-amz-date'] else headers['X-Amz-Date'] = this.getDateTime() } delete headers.Authorization delete headers.authorization } }
...
delete headers.Authorization
delete headers.authorization
}
}
RequestSigner.prototype.sign = function() {
if (!this.parsedPath) this.prepareRequest()
if (this.request.signQuery) {
this.parsedPath.query['X-Amz-Signature'] = this.signature()
} else {
this.request.headers.Authorization = this.authHeader()
}
...
sign = function () { if (!this.parsedPath) this.prepareRequest() if (this.request.signQuery) { this.parsedPath.query['X-Amz-Signature'] = this.signature() } else { this.request.headers.Authorization = this.authHeader() } this.request.path = this.formatPath() return this.request }
...
// alternatively (as aws4 can infer the host):
opts = {service: 'sqs', region: 'us-east-1', path: '/?Action=ListQueues'}
// alternatively (as us-east-1 is default):
opts = {service: 'sqs', path: '/?Action=ListQueues'}
aws4.sign(opts) // assumes AWS credentials are available in process.env
console.log(opts)
/*
{
host: 'sqs.us-east-1.amazonaws.com',
path: '/?Action=ListQueues',
headers: {
...
signature = function () { var date = this.getDate(), cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(), kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey) if (!kCredentials) { kDate = hmac('AWS4' + this.credentials.secretAccessKey, date) kRegion = hmac(kDate, this.region) kService = hmac(kRegion, this.service) kCredentials = hmac(kService, 'aws4_request') credentialsCache.set(cacheKey, kCredentials) } return hmac(kCredentials, this.stringToSign(), 'hex') }
...
// Generate CodeCommit Git access password
var signer = new aws4.RequestSigner({
service: 'codecommit',
host: 'git-codecommit.us-east-1.amazonaws.com',
method: 'GIT',
path: '/v1/repos/MyAwesomeRepo',
})
var password = signer.getDateTime() + 'Z' + signer.signature()
```
API
---
### aws4.sign(requestOptions, [credentials])
...
signedHeaders = function () { return Object.keys(this.request.headers) .map(function(key) { return key.toLowerCase() }) .sort() .join(';') }
...
if (query['X-Amz-Date'])
this.datetime = query['X-Amz-Date']
else
query['X-Amz-Date'] = this.getDateTime()
query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString()
query['X-Amz-SignedHeaders'] = this.signedHeaders()
} else {
if (!request.doNotModifyHeaders && !this.isCodeCommitGit) {
if (request.body && !headers['Content-Type'] && !headers['content-type'])
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
...
stringToSign = function () { return [ 'AWS4-HMAC-SHA256', this.getDateTime(), this.credentialString(), hash(this.canonicalString(), 'hex'), ].join('\n') }
...
if (!kCredentials) {
kDate = hmac('AWS4' + this.credentials.secretAccessKey, date)
kRegion = hmac(kDate, this.region)
kService = hmac(kRegion, this.service)
kCredentials = hmac(kService, 'aws4_request')
credentialsCache.set(cacheKey, kCredentials)
}
return hmac(kCredentials, this.stringToSign(), 'hex')
}
RequestSigner.prototype.stringToSign = function() {
return [
'AWS4-HMAC-SHA256',
this.getDateTime(),
this.credentialString(),
...