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 | |
} |
This comment has been minimized.
This comment has been minimized.
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
This comment has been minimized.
Here's how I used Aws4Signer, to sign the HTTP request, in a more OOP manner:
http://www.amihaiemil.com/2017/02/18/decorators-with-tunnels.html