Soto icon

Soto

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.