Growing Into AWS

My near-term goal is to grow into using AWS more, and this project was a step in the right direction. The objective was to get a basic hello-world endpoint with:

  • API Gateway in front
  • Lambda serving a hello-world response
  • Cognito doing auth

Simple, right? Just make an endpoint accessible to the world, provide a basic response to know that it works, and limit who can interact with it. This provides the scaffold for building more; add more routes backed by more functions, make the functions hit a datastore like Dynamo DB, and suddenly I have an arbitrarily-scalable CRUD endpoint. Seems cool. So how do we get there?

Organizing Concept: AWS Is A Platypus

Each time I come back to AWS, I am struck by the total disharmony of design across components. SAM lets you use a CLI to spin up a stack, but you need to use Cloudformation to take it down. SAM appears to be a superset of Cloudformation, but CF is also supported and sometimes preferred?? In the AWS CLI, there is the s3 set of commands, and also the s3api set of commands, with substantial overlap.

"There should be one-- and preferably only one --obvious way to do it."
-- Source: `python -c "import this" | tail -n 7  | head -n 1`

It is very much like a platypus: a weird design that might seem familiar locally, but definitely isn't globally, and only really makes sense when you see all the pieces together. So this is a very small setup that still does something nontrivial.

Starting Small

From-Zero Setup

Make an AWS Account if you don't have one.

Make an admin user to work with.

If you're confused about IAM, try this.

Install CLIs (because of course you need two)

Get the SAM CLI.

Also get the AWS CLI.

Suggested Tooling

I wasted half an hour debugging a Cloudformation template problem before I installed a linter that pinpointed the problem for me. Don't be like me: use kddejong.vscode-cfn-lint in the VSCode extensions from the start. If you forget a dash and accidentally try to give a dict to an array property, this will tell you.

The Steps

There are stages to this work:

  1. Do the SAM hello-world example as a base
  2. Add on the Cognito resources
  3. Configure Cognito hosted auth
  4. Make a user and get a login token
  5. Test the token in API Gateway
  6. Use it

Do the SAM hello-world example

From here.

High level paraphrase of the article

sam init to start a project. I use python 3.8, do what works for you. You should have a folder structure kinda like this. You won't have templategood.yaml; that's my own addition.

Basic project template

Install local testing requirements locally with pip install pytest pytest-mock --user. Try running them with pytest, which should work under a normal default setup, but doesn't for me; I need to assert the PYTHONPATH like so:

Running pytest

OK! You have something worth deploying. Bundle up your code with

sam build

and deploy it with

sam deploy --guided

Almost all the defaults are correct. My first time I needed to agree to create roles, IIRC, but don't have that problem on subsequent runs.

You should be able to watch the deploy complete, then see it in your Cloudwatch stack list with aws cloudformation list-stacks.

Try hitting it at the invocation URL the deployment gave you. Remember the URL format should be https://<host>/<environment>/hello for the default template. If you skip the trailing hello, it will fail and you will be confused. This hit should succeed.

Great! Now tear it down. (The command is aws cloudformation delete-stack --stack-name sam-app --region region. Note how we have to use a different CLI for this lifecycle task.)

Adding Cognito Resources

Cognito has two kinds of auth under the same service, basically: User pools and Identity pools. User pools are similar to the standard signup/login identity flow most places use. Identity pools map the person logging in to an IAM role, giving you permissions management at the IAM level. For now, just think about user pools; we're adding one of those.

In the Resources block of your template.yaml, add something like this:

  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: swimming-pool
      Policies:
        PasswordPolicy:
          MinimumLength: 8
      UsernameAttributes:
        - email
      Schema:
        - AttributeDataType: String
          Name: email
          Required: false

to declare the pool. You can use whatever pool name you want. We also need a user pool client, so add something like:

  MyCognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      UserPoolId: !Ref CognitoUserPool
      ClientName: user-pool-client
      GenerateSecret: false

note the !Ref; it lets us refer to some other thing in our Cloudformation template to populate that variable, rather than hardcode a value. This User Pool Client will be configured to use the AWS hosted login flow.

We also need to assign the Cognito User Pool as an authorizer on the endpoint we are building. In the basic hello-world example, this API got built by implication of our work. Here, we need to include it so we can add those Authorizer blocks.

  MyAPI:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Auth:
        DefaultAuthorizer: CognitoAuthorizer
        Authorizers:
          CognitoAuthorizer:
            UserPoolArn: !GetAtt CognitoUserPool.Arn

And finally, wire the function up to the API:

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref MyAPI
            Path: /hello
            Method: get

This block should look very similar to the template you started with. Note the RestApiId property we added.

If you build and deploy this, it should spin up your Cognito resources as well, so do that.

sam build sam deploy

Adding Cognito Hosted Auth

Go to https://console.aws.amazon.com/cognito and Manage User Pools. Pick the pool you made, then go to App Integration > Domain Name. This is where we will pick a domain to host the auth Amazon will hold for us. Choose anything, but you won't be allowed to choose something someone already chose.

Next go to App Integration > App Client Settings. Make it look like this:

Auth integration

Those two things together should unlock the Launch Hosted UI link at the bottom of the page. Open that in a new tab, then go to your General Settings > Users and Groups.

Create a User

Just fill in something basic for now as a test user. Remember that the phone format in the US needs to be +15555555555; AWS will yell at you if you forget the +1.

Login With That User

In the Hosted Auth tab you opened, login with that user. The redirect URL will look like:

<Your Redirect URL>#id_token=areallyreallylongstringofnumbersandletters&access_token=adifferentreallyreallylongstringofnumbersandletters&expires_in=3600&token_type=Bearer

That ID token is what you want. Validate it at JWT.io; the decoded token should have Header, Payload, and Signature, and the signature should be valid (at the bottom below your token).

Test The Token In APIGateway

In the AWS Console, go to you API Gateway, go to the authorizers (that should already exist because you changed the template), and hit that Test button. Put the JWT (the id_token you validated) in, and you should get a validity confirmation.

Auth integration

Use That Token!

In Postman, hit your API Gateway (shaped like https://<host>/<environment>/hello) with a header of Authorization of your JWT token (no Bearer). You should get the valid response!

Room For Growth

This endpoint uses an Implicit Grant; it just gives the auth-worthy creds back to the user. This is a risk, because those can be intercepted. The internet, and the docs, imply that implicit grants are to be avoided if possible.

Next Steps (/Exercises For The Reader)

  1. Add more APIs
  2. Use more intelligent security
  3. Touch a datastore