Skip to content

Instantly share code, notes, and snippets.

@ctalladen78
Last active January 31, 2024 17:37
Show Gist options
  • Save ctalladen78/cb74ac4b332cb909ce04bcf6b9590643 to your computer and use it in GitHub Desktop.
Save ctalladen78/cb74ac4b332cb909ce04bcf6b9590643 to your computer and use it in GitHub Desktop.
Golang, AWS SDK, Cognito and API Gateway

https://benincosa.com/?p=3714

The situation is as follows:

Create an application with the serverless framework. This uses API Gateway, Lambda, and all kinds of cool stuff. Authenticate on the application using Cognito. Write a client that can call the API created by API gateway in Go. Steps 1-2 are covered everywhere on the internet. My favorite reference is this serverless stack tutorial. It is gold and covers so much.

But step 3 is pretty poorly documented. What I found are several old libraries that do v4 signing but are no longer maintained and shouldn’t be used. Below I document a solution that works using the AWS Go SDK.

  1. Cognito Setup User Pool: I have a user pool and a corresponding App Client. The App Client for the command line Go code that I’m writing is separate from the App Client that is used by the web interface. You’ll need to note both of these:
UserPoolId: us-east-1_123456789
AppClientId: 123456789abcdefghijklmnopq

Federated Identity: In addition an identity pool which uses the user pool ID should be created. This will also give you an identification

IdentityPoolId: us-east-1:abc13813-4444-4444-4444-123456789abc Make sure you can login and out and that this cognito stuff works. I used the serverless stack tutorial above and I could log in and out with the web interface.

  1. API Gateway Setup I’m using the serverless framework to create my stack. A few notes about it:

The Method Request should specify that it requires Auth: AWS_IAM The Cognito Identity pool has an authenticated and unathenticated role. Make sure that role has the proper permissions to call the lambda functions. As an example, my cognito identity pool authenticated role has the following properties:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": [
                "arn:aws:execute-api:us-east-1:*:123456789/*/*/*"
            ]
        }
    ]
}


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": [
                "arn:aws:execute-api:us-east-1:*:123456789/*/*/*"
            ]
        }
    ]
}

You would make sure the execute-api is set to your correct API Gateway ID. You can find this in the stages portion where you invoke the API:

InvokeURL: https://123456789.execute-api.us-east-1.amazonaws.com/dev 3. Golang By leaving out the error checking and structure I’ll make this as simple as possible to do. You’ll probably want to do some caching of some sorts and of course check for errors. In the steps below we will turn our Cognito username and password into IAM credentials that assume the role of executing the API, after which we will use them invoke the API.

3.1 Create aws session

ses, _ := session.NewSession(&aws.Config{Region: aws.String("us-east-1")})
1
ses, _ := session.NewSession(&aws.Config{Region: aws.String("us-east-1")})

3.2 Authenticate user from Cognito Identity Provider Your cognito user has a username and password. (I’m using an email). Authenticate this:

params := &cognitoidentityprovider.InitiateAuthInput{
    AuthFlow: aws.String("USER_PASSWORD_AUTH"),
    AuthParameters: map[string]*string{
      "USERNAME": aws.String("[email protected]"),
      "PASSWORD": aws.String("doremefasolatido"),
    },
    ClientId: aws.String("123456789abcdefghijklmnopq"), // this is the app client ID
  }
cip := cognitoidentityprovider.New(ses)
authResp, _ := cip.InitiateAuth(params)

params := &cognitoidentityprovider.InitiateAuthInput{
    AuthFlow: aws.String("USER_PASSWORD_AUTH"),
    AuthParameters: map[string]*string{
      "USERNAME": aws.String("[email protected]"),
      "PASSWORD": aws.String("doremefasolatido"),
    },
    ClientId: aws.String("123456789abcdefghijklmnopq"), // this is the app client ID
  }
cip := cognitoidentityprovider.New(ses)
authResp, _ := cip.InitiateAuth(params)

The AuthResp will contain the IdToken, AccessToken, RefreshToken, etc. What you need is an IAM user. Notice that you’re just using the AppClientID and not the UserPoolId. I thought this was a little strange but since the AppClientId belongs to the UserPoolId I guess it works.

3.3 Get ID from Cognito Identity This section follows (to some extent) the documentation in the Overview section of the cognito Identity API Reference. (Seriously, Amazon, a few examples would be nice) Now that we have the IdToken we can use that to get the ID of the user:

svc := cognitoidentity.New(ses)
idRes, _ := svc.GetId(&cognitoidentity.GetIdInput{
    IdentityPoolId: aws.String("us-east-1:123456789-444-4444-123456789abc"),
    Logins: map[string]*string{
      "cognito-idp.us-east-1.amazonaws.com/us-east-1_123456789": authResp.AuthenticationResult.IdToken,
    },
  })

svc := cognitoidentity.New(ses)
idRes, _ := svc.GetId(&cognitoidentity.GetIdInput{
    IdentityPoolId: aws.String("us-east-1:123456789-444-4444-123456789abc"),
    Logins: map[string]*string{
      "cognito-idp.us-east-1.amazonaws.com/us-east-1_123456789": authResp.AuthenticationResult.IdToken,
    },
  })

This result gives us a user id which we can now get credentials:

credRes, _ := svc.GetCredentialsForIdentity(&cognitoidentity.GetCredentialsForIdentityInput{
    IdentityId: idRes.IdentityId,
    Logins: map[string]*string{
      "cognito-idp.us-east-1.amazonaws.com/us-east-1_123456789": authResp.AuthenticationResult.IdToken,
    },
  })

credRes, _ := svc.GetCredentialsForIdentity(&cognitoidentity.GetCredentialsForIdentityInput{
    IdentityId: idRes.IdentityId,
    Logins: map[string]*string{
      "cognito-idp.us-east-1.amazonaws.com/us-east-1_123456789": authResp.AuthenticationResult.IdToken,
    },
  })

Cool, now if you look at credRes you have IAM AccessKeyId, SecretKey, SessionToken! Everything we need to now call our API.

3.4 Invoke the API Gateway with a Signed Request To invoke the API we create a new request like we would if it were unauthenticated:

url := "https://123456789.execute-api.us-east-1.amazonaws.com/dev/list"
client := new(http.Client)
req, _ := http.NewRequest("GET", url, nil)



url := "https://123456789.execute-api.us-east-1.amazonaws.com/dev/list"
client := new(http.Client)
req, _ := http.NewRequest("GET", url, nil)

In this case I’m calling the “/list” resource and using the GET method.

Now the trick here is that we need to sign the request. With the SDK we now have a library that does that for us:

v := v4.NewSigner(credentials.NewStaticCredentials(
    *credRes.Credentials.AccessKeyId,
    *credRes.Credentials.SecretKey,
    *credRes.Credentials.SessionToken,
  ))

v.Sign(req, nil, "execute-api", "us-east-1", time.Now())

v := v4.NewSigner(credentials.NewStaticCredentials(
    *credRes.Credentials.AccessKeyId,
    *credRes.Credentials.SecretKey,
    *credRes.Credentials.SessionToken,
  ))
 
v.Sign(req, nil, "execute-api", "us-east-1", time.Now())

Notice that we pass in the req variable. This step will add headers to the request that will authorize our request. Finally do the request:

resp, _ := client.Do(req)
1
resp, _ := client.Do(req)
Check out the response:

b, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s\n", b)

b, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s\n", b)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment