Signing URLs and headers
I have previously written an article on generating pre-signed URLs for S3. This article though covers the situation where a signed URL or set of signed headers are required for IAM authentication outside of the AWS service APIs. Below is detailed two different methods for doing this.
1) Using a service object
The first method uses the service object from a Soto service library. This is the easier of the two methods but requires an AWSClient
to manage your AWS credentials and that you have a service object with correct signing name. The method uses either AWSService.signURL
for signing URLS or AWSService.signHeaders
for generating signed headers.
In the example below we have a function for signing requests to a AWS managed Elasticsearch instance. The function uses signHeaders
as the signed URL query parameters would confuse the Elasticsearch instance.
func elasticSearchExecute(
url: URL,
method: HTTPMethod,
headers: HTTPHeaders,
body: ByteBuffer? = nil
) -> EventLoopFuture<HTTPClient.Response> {
let es = ElasticsearchService(client: awsClient, region: .useast1)
return es.signHeaders(
url: url,
httpMethod: method,
headers: headers,
body: body.map { .byteBuffer($0) } ?? .empty
).flatMap { signedHeaders in
let request = try! HTTPClient.Request(
url: url,
method: method,
headers: signedHeaders,
body: body.map { .byteBuffer($0) }
)
return httpClient.execute(request: request, logger: logger)
}
}
2) Using the Soto signer directly
The second method uses SotoSignerV4
directly. This requires you have your AWS credentials to hand to initialize the AWSSignerV4
. We call AWSSignerV4.processURL
on the URL before signing it to clean it up. Cleaning up the URL means sorting the query parameters in alphabetical order and percent encoding as AWS requires.
In the example below we are signing a request to an API Gateway REST interface. Please note for API Gateway interfaces the signing name is execute-api
.
import SotoSignerV4
func apiGatewayExecute(
url: URL,
method: HTTPMethod,
headers: HTTPHeaders,
body: ByteBuffer? = nil
) -> EventLoopFuture<HTTPClient.Response> {
let credentials: Credential = StaticCredential(
accessKeyId: "_MYACCESSKEY_",
secretAccessKey: "_MYSECRETACCESSKEY_"
)
let signer = AWSSigner(credentials: credentials, name: "execute-api", region: "us-east-1")
// clean up URL
let processedURL = signer.processURL(url: url)!
let signedHeaders = signer.signHeaders(
url: processedURL,
method: method,
headers: headers,
body: body.map { .byteBuffer($0) }
)
let request = try! HTTPClient.Request(
url: processedURL,
method: method,
headers: signedHeaders,
body: body.map { .byteBuffer($0) }
)
return httpClient.execute(request: request, logger: logger)
}
Both of these examples use the swift-server AsyncHTTPClient but you should be able to use any HTTP client using these methods.