Skip to content

Instantly share code, notes, and snippets.

@devilelephant
Last active October 21, 2019 08:24
Show Gist options
  • Save devilelephant/4ae94f8e9f24b6056733 to your computer and use it in GitHub Desktop.
Save devilelephant/4ae94f8e9f24b6056733 to your computer and use it in GitHub Desktop.
Java/Groovy example of using Amazon AWS AWS4Signer class to sign requests (in our case elasticsearch calls)
package com.clario.aws
import com.amazonaws.DefaultRequest
import com.amazonaws.SignableRequest
import com.amazonaws.auth.AWS4Signer
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.http.HttpMethodName
import groovy.util.logging.Slf4j
import org.apache.http.client.utils.URLEncodedUtils
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpRequest
/**
* Sign a url using Amazon {@link AWS4Signer}.
*
* Note: if you get a 403 signature error you can put a breakpoint at the end of Amazon's AWS4Signer.sign()
* (or turn on debug level on com.amazonaws.auth package ) to capture the generated canonicalRequest String and compare
* it to Amazon's expected string that is returned with the error.
*
* @author George Coller
*/
@Slf4j
class V4RequestSigner {
private final String regionName
private final String serviceName
private final AWSCredentialsProvider awsCredentialsProvider
V4RequestSigner(AWSCredentialsProvider awsCredentialsProvider, String regionName, String serviceName) {
this.regionName = regionName
this.awsCredentialsProvider = awsCredentialsProvider
this.serviceName = serviceName
}
void signRequest(HttpRequest request, byte[] body) {
def headers = request.headers
if (body == null || body.length == 0) {
// Signer wanted the value when zero to be empty-string but Spring's rest template tries to parse it to a Long. Easier to just remove the header if it exits.
headers.keySet().findAll { it.equalsIgnoreCase('Content-Length') }.each { headers.remove(it) }
}
def signableRequest = makeSignableRequest(request, body)
headers.clear()
headers.putAll(signHeaders(signableRequest))
}
HttpHeaders signHeaders(SignableRequest<String> signableRequest) {
AWS4Signer signer = new AWS4Signer(false)
signer.regionName = regionName
signer.serviceName = serviceName
signer.sign(signableRequest, awsCredentialsProvider.credentials)
def headers = new HttpHeaders()
signableRequest.headers.each { k, v ->
headers.add(k, v)
}
return headers
}
SignableRequest<String> makeSignableRequest(HttpRequest httpRequest, byte[] bytes) {
def request = new DefaultRequest<String>(serviceName)
// Separate URI base and resource path
def uri = httpRequest.URI
request.setEndpoint(new URI(uri.scheme, null, uri.host, uri.port, '', '', ''))
def rawPath = uri.rawPath.replaceAll('\\+', '%2B') // Signer wasn't happy about urls with spaces, wanted all '+' to be encoded as %2B.
request.setResourcePath(rawPath)
URLEncodedUtils.parse(uri, 'UTF-8').each { nameValue ->
request.addParameter(nameValue.name, nameValue.value)
}
request.setHttpMethod(HttpMethodName.valueOf(httpRequest.method.toString()))
request.setHeaders(httpRequest.headers.collectEntries { k, v -> [k, v.join(',')] } as Map<String, String>)
request.setContent(new ByteArrayInputStream(bytes))
return request
}
}
// Snippit of how to inject the AWS4Signer class into a Spring RestTemplate so it signs every REST call:
@Bean
RestTemplate restTemplate() {
def requestFactory = new HttpComponentsClientHttpRequestFactory()
requestFactory.setReadTimeout(60_000)
requestFactory.setConnectTimeout(5_000)
def template = new RestTemplate(requestFactory)
template.interceptors.add(new ClientHttpRequestInterceptor() {
// In our case we're using us east 1 region and are going against the AWS elasticsearch endpoint
final signer = new V4RequestSigner(awsCredentials(), 'us-east-1', 'es')
@Override
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
signer.signRequest(request, body)
return execution.execute(request, body);
}
})
return template
}
@devilelephant
Copy link
Author

Updated April 27 2018. Signer failed when URL had spaces (e.g "/files/My Dumb File.txt") because it expected them to be escaped with %2B instead of '+'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment