3

Hello StackOverflow AWS Gophers,

I'm implementing a CLI with the excellent cobra/viper packages from spf13. We have an Athena database fronted by an API Gateway endpoint, which authenticates with IAM.

That is, in order to interact with its endpoints by using Postman, I have to define AWS Signature as Authorization method, define the corresponding AWS id/secret and then in the Headers there will be X-Amz-Security-Token and others. Nothing unusual, works as expected.

Since I'm new to Go, I was a bit shocked to see that there are no examples to do this simple HTTP GET request with the aws-sdk-go itself... I'm trying to use the shared credentials provider (~/.aws/credentials), as demonstrated for the S3 client Go code snippets from re:Invent 2015:

req := request.New(nil)

How can I accomplish this seemingly easy feat in 2019 without having to resort to self-cooked net/http and therefore having to manually read ~/.aws/credentials or worse, go with os.Getenv and other ugly hacks?

Any Go code samples interacting as client would be super helpful. No Golang Lambda/server examples, please, there's plenty of those out there.

brainstorm
  • 720
  • 7
  • 24
  • 1
    you are not able to get informations from [this](https://github.com/umccr/cli/blob/ba95fcf15fb2c744c09e92ad16393751533e664a/cmd/find.go#L41) function right? – Tinwor May 16 '19 at 09:23
  • Yes, that's right, this function is the one I should be writing, ideally with `aws-sdk-go`. – brainstorm May 16 '19 at 21:05

4 Answers4

5

The solution below uses aws-sdk-go-v2 https://github.com/aws/aws-sdk-go-v2

// A AWS SDK session is created because the HTTP API is secured using a
// IAM authorizer. As such, we need AWS client credentials and a
// session to properly sign the request.
cfg, err := external.LoadDefaultAWSConfig(
    external.WithSharedConfigProfile(profile),
)
if err != nil {
    fmt.Println("unable to create an AWS session for the provided profile")
    return
}


req, _ := http.NewRequest(http.MethodGet, "", nil)
req = req.WithContext(ctx)
signer := v4.NewSigner(cfg.Credentials)
_, err = signer.Sign(req, nil, "execute-api", cfg.Region, time.Now())
if err != nil {
    fmt.Printf("failed to sign request: (%v)\n", err)
    return
}

res, err := http.DefaultClient.Do(req)
if err != nil {
    fmt.Printf("failed to call remote service: (%v)\n", err)
    return
}

defer res.Body.Close()
if res.StatusCode != 200 {
    fmt.Printf("service returned a status not 200: (%d)\n", res.StatusCode)
    return
}
Antoine Delia
  • 1,728
  • 5
  • 26
  • 42
  • Thanks for this @craftsource, I see that this snippet is using `aws-sdk-go-v2` instead of v1... I go get'd the appropriate one but I'm getting `cannot use cfg.Credentials (type "github.com/aws/aws-sdk-go-v2/aws".CredentialsProvider) as type *credentials.Credentials in argument to v4.NewSigner` when pasting it as-is, unfortunately :/ – brainstorm May 18 '19 at 07:30
  • 1
    You need to use the aws-sdk-go-v2 v4 Signer. – craftsource May 18 '19 at 18:11
  • Thanks much! I was not importing the right sdk, `import v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"` and off we go :) – brainstorm May 19 '19 at 23:15
5

Unfortunately, it seems that the library has been updated since the accepted answer was written and the solution no longer is the same. After some trial and error, this appears to be the more current method of handling the signing (using https://pkg.go.dev/github.com/aws/aws-sdk-go-v2):

import (
    "context"
    "net/http"
    "time"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)

func main() {
    // Context is not being used in this example.
    cfg, err := config.LoadDefaultConfig(context.TODO())

    if err != nil {
        // Handle error.
    }

    credentials, err := cfg.Credentials.Retrieve(context.TODO())

    if err != nil {
        // Handle error.
    }

    // The signer requires a payload hash. This hash is for an empty payload.
    hash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    req, _ := http.NewRequest(http.MethodGet, "api-gw-url", nil)
    signer := v4.NewSigner()
    err = signer.SignHTTP(context.TODO(), credentials, req, hash, "execute-api", cfg.Region, time.Now())

    if err != nil {
        // Handle error.
    }

    // Use `req`
}
Alan Wong
  • 66
  • 1
  • 1
1

The first argument to request.New is aws.Config, where you can send credentials.

https://github.com/aws/aws-sdk-go/blob/master/aws/request/request.go#L99 https://docs.aws.amazon.com/sdk-for-go/api/aws/#Config

There are multiple ways to create credentials object: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html

For example using static values:

creds:= credentials.NewStaticCredentials("AKID", "SECRET_KEY", "TOKEN")
req := request.New(aws.Config{Credentials: creds}, ...)
Alex Pliutau
  • 21,392
  • 27
  • 113
  • 143
  • Correct, defining static credentials in code is a bad idea, therefore I go with what the documentation says: "When you initialize a new service client without providing any credential arguments, the SDK uses the default credential provider chain to find AWS credentials." ... which is passing `nil` to the new method, right? Doesn't work, unfortunately. – brainstorm May 16 '19 at 21:07
  • 1
    I guess the new way in 2019 is to go for sth like: `req := request.New(defaults.Get().Config, metadata.ClientInfo, defaults.Get().Handlers, client.DefaultRetryer())`, but still, no concrete simple examples available, unfortunately :/ – brainstorm May 17 '19 at 02:05
0

I'm pretty new to go myself (3rd day learning go) but from watching the video you posted with the S3 example and reading through the source code (for the s3 service and request module) here is my understanding (which I'm hoping helps).

If you look at the code for the s3.New() function aws-sdk-go/service/s3/service.go

func New(p client.ConfigProvider, cfgs ...*aws.Config) *S3 {
c := p.ClientConfig(EndpointsID, cfgs...)
return newClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, .SigningName) }

As opposed to request.New() function aws-sdk-go/aws/request/request.go

func New(cfg aws.Config, clientInfo metadata.ClientInfo, handlers Handlers,
retryer Retryer, operation *Operation, params interface{}, data interface{}) *Request { ...

As you can see in the s3 scenario the *aws.Config struct is a pointer, and so is probably initialized / populated elsewhere. As opposed to the request function where the aws.Config is a parameter. So I am guessing the request module is probably a very low level module which doesn't get the shared credentials automatically.

Now, seeing as you will be interacting with API gateway I had a look at that service specifically to see if there was something similar. I looked at aws-sdk-go/service/apigateway/service.go

func New(p client.ConfigProvider, cfgs ...*aws.Config) *APIGateway {
c := p.ClientConfig(EndpointsID, cfgs...)
return newClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) }...

Which looks pretty much the same as the s3 client, so perhaps try using that and see how you go?

Grizzle
  • 519
  • 3
  • 13
  • Hi Grizzle, thanks for the walkthrough but the craftsource's answer was accepted a few days ago, meaning that I managed to make it work with `aws-sdk-go-v2` SDK (the newer version of AWS Go SDK). You can peek at my actual, working code over here: https://github.com/umccr/cli/blob/master/cmd/find.go#L46 – brainstorm May 23 '19 at 20:15