Using API Gateway for Authorization and Authentication

Miguel Ruiz
Expedia Group Technology
8 min readSep 10, 2018

We have covered quite a bit so far on how our application is architected. If you missed out on following it, check out our other blog post in this series:

You have many different options when it comes to implementing authorization and authentication. We had no particular requirements other than there needed to be some, and that it couldn’t be “static” (e.g. a hardcoded username and password passed along with everything request).

We looked at different options before settling on API Gateway. Being a NodeJS app, Passport was the most popular choice. I had also built my own authentication method for another project (side note: never do this). Ultimately, though, we wanted something we didn’t have to maintain the health of.

API Gateway to the rescue

API Gateway seemed like a perfect fit except for one thing: at the time, you couldn’t put API Gateway in front of resources inside a VPC.

After some discussion, we decided to punt. Our application was only running in a lab environment, and it would be some time before it was exposed to the public internet. In the mean time, we decided that whatever we used for authorization and authentication, it would have to be implemented in a way similar to API Gateway; something would sit in front of our application to handle this responsibility. This functionality would not be embedded in our application code. If we chose something that worked in a similar way, then it would be easier to make the switch if API Gateway ever became viable.

So we waited and researched other options. We didn’t know if AWS would ever support such a scenario. All we could find were some random forum posts from the official forum saying they were looking into it.

I mean…NLBs to the rescue!

In late 2017, we were thrown a lifeline. AWS released their Network Load Balancer which finally gave us the ability to expose resources inside our VPC out through API Gateway.

I don’t want to use an NLB Too bad. At time of writing, if you want to use API Gateway and VPCs, it has to be behind an NLB.

How to make this happen

We’ll be creating cloud formation templates for the following…

1. Network Load Balancer
2. Network Load Balancer Targets
3. API Gateway
4. API Gateway Deployment

Can’t I use the console? You’re welcome to figure that out. We deploy our application using CFTs. There’s no benefit doing this through the console unless you intend to manually configure all your resources, across all your environments. But don’t do that. Using CFTs will make deploying your application much easier.

Network Load Balancer and its Targets

The CFT for the load balancer is pretty self explanatory. It looks like this:

Description: Load Balancer for ApplicationAPI
Parameters:
Stage:
Type: String
Description: Deployment stage for this instance of ApplicationAPI
Subnets:
Type: List<AWS::EC2::Subnet::Id>
Description: List of Subnets, need to be across 2 availability zones
Resources:
ApplicationAPILoadBalancer:
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
Properties:
Name:
!Join
- ''
- - 'applicationapi-'
- !Ref Stage
- '-lb'
Scheme: internal
Subnets: !Ref Subnets
Type: network
IpAddressType: ipv4

It takes only a Stage name, which can be anything that makes sense to you. Also to be supplied is a list of subnets you expect your ECS service to be running in.

Explanation of Stage name Stage is a variable we use across all our CFTs, so you’ll see it a lot. We use it to differentiate between what are traditionally called “environments”. So we have a “dev” stage, a “staging” stage, and others as needed.

Next we need to set up the targets. The CFT looks something like this:

Description: Load Balancer for ApplicationAPI
Parameters:
LoadBalancerArn:
Type: String
Description: ARN of a load balancer instance
Stage:
Type: String
Description: Deployment stage for this instance of ApplicationAPI
Vpc:
Type: AWS::EC2::VPC::Id
Description: VPC ID
Resources:
ApplicationAPITargetGroup:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
Name:
!Join
- ''
- - 'applicationapi-'
- !Ref Stage
- '-targetgroup'
Port: 80
Protocol: TCP
TargetType: ip
VpcId: !Ref Vpc
ApplicationAPIListener:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
DefaultActions:
- Type: forward
TargetGroupArn:
Ref: ApplicationAPITargetGroup
LoadBalancerArn:
Ref: LoadBalancerArn
Port: 80
Protocol: TCP

The TargetType: ip attribute is critical to getting this working with Fargate. The NLB’s target group needs to know that it’s balancing across IP Addresses, rather than instance ids. The target will be associated with the ECS service in another CFT, and it’s from that connection that Fargate will be able to associate the IP Address with balancer target groups.

Note there’s also a AWS::ElasticLoadBalancingV2::Listener, which creates a listener for our NLB, checking for connection requests and forwarding them to the target groups.

API Gateway

This API Gateway sits in front of an application running in Fargate. That application has routes exposed and returns valid HTTP status codes depending on the situation. With that in place, the API Gateway can simply allow requests and responses pass back and forth.

A quick note about our application It’s a NodeJS app using Koa as a web server. It has routes defined by means of the koa-router module and is strict about ensuring that all output is HTTP friendly. This allows API Gateway to act as a simple proxy. We could’ve pushed some of that responsibility on to API Gateway, but it didn’t make sense given some of our other application requirements.

It’s possible to define a bunch of AWS::ApiGateway::Method in the CFT for your API Gateway to define the routes, but there are some things you can’t take advantage of if you go that way, such as setting a VPC Link.

So, you’ll need to define a AWS::ApiGateway::RestAPI with a Body containing the definition for your routes. The easiest way to bootstrap this CFT is to manually build an API Gateway in the console, deploy it, and then use the export option. To do that, select Stages > [stage name] > Export > Export as Swagger + API Gateway Extensions. Copy/Paste the output as the Body for your AWS::ApiGateway::RestAPI, and you should be on your way.

Wait, why am I building this in the console first? Some of the options in the console don’t have clear cut analogs in cloud formation. CORS is the perfect example. It’s a single button in the console, but there’s no enable-cors flag in cloud formation. You have to add specific header definitions you’ll see below. We simply found it easier to start with the console first.

As a last step, delete the API Gateway you just created. You’ll deploy it using a CFT next time.

After bootstrapping the template and continuing to develop it, it looks something like this:

Description: APIGateway in front of ApplicationAPI
Parameters:
NlbArn:
Type: String
Description: ARN of the NLB for ApplicationAPI
NlbDns:
Type: String
Description: DNS name of the NLB for ApplicationAPI
Stage:
Type: String
Description: Deployment Stage
Resources:
ApplicationAPIVpcLink:
Type: "AWS::ApiGateway::VpcLink"
Properties:
Description: VPC Link to ApplicationAPI NLB
Name: !Sub applicationapi-${Stage}-vpclink
TargetArns:
- !Ref NlbArn
ApplicationApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Description: API for accessing applicationAPI
EndpointConfiguration:
Types:
- REGIONAL
Name: !Sub applicationapi-${Stage}-api
Body:
swagger: "2.0"
schemes:
- "https"
paths:
/_application:
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\"statusCode\": 200}"
passthroughBehavior: "when_no_match"
type: "mock"
/_application/{token}:
get:
produces:
- "application/json"
parameters:
- name: "token"
in: "path"
required: true
type: "string"
security:
- sigv4: []
x-amazon-apigateway-integration:
uri:
Fn::Sub: http://${NlbDns}/_application/{token}
requestParameters:
integration.request.path.token: "method.request.path.token"
passthroughBehavior: "when_no_match"
connectionType: "VPC_LINK"
connectionId:
Ref: ApplicationAPIVpcLink
httpMethod: "GET"
type: "http_proxy"
put:
produces:
- "application/json"
parameters:
- name: "token"
in: "path"
required: true
type: "string"
security:
- sigv4: []
x-amazon-apigateway-integration:
uri:
Fn::Sub: http://${NlbDns}/_application/{token}
requestParameters:
integration.request.path.token: "method.request.path.token"
passthroughBehavior: "when_no_match"
connectionType: "VPC_LINK"
connectionId:
Ref: ApplicationAPIVpcLink
httpMethod: "PUT"
type: "http_proxy"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\"statusCode\": 200}"
passthroughBehavior: "when_no_match"
type: "mock"
/health:
get:
produces:
- "application/json"
security:
- sigv4: []
x-amazon-apigateway-integration:
uri:
Fn::Sub: http://${NlbDns}/health
passthroughBehavior: "when_no_match"
connectionType: "VPC_LINK"
connectionId:
Ref: ApplicationAPIVpcLink
httpMethod: "GET"
type: "http_proxy"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\"statusCode\": 200}"
passthroughBehavior: "when_no_match"
type: "mock"
securityDefinitions:
sigv4:
type: "apiKey"
name: "Authorization"
in: "header"
x-amazon-apigateway-authtype: "awsSigv4"
definitions:
Empty:
type: "object"
title: "Empty Schema"
x-amazon-apigateway-binary-media-types:
- "multipart/form-data"
ApplicationAPI4XXGatewayResponses:
Type: AWS::ApiGateway::GatewayResponse
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId: !Ref ApplicationApi
ApplicationAPI5XXGatewayResponses:
Type: AWS::ApiGateway::GatewayResponse
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId: !Ref ApplicationApi
Outputs:
ApplicationApiId:
Description: API Id
Value: !Ref ApplicationApi

The first thing defined is a VPC Link. This is what will allow your API Gateway to talk to the application running in your VPC. It’s pretty straightforward, the only thing it needs to know about is the NLB Arn.

Next is our AWS::ApiGateway::RestApi. All our paths are defined, along with each supported verb, and what content types to expect.

What’s up with the underscore? We do our best to follow RESTful principles, but we needed a way to communicate that some routes are helper routes that may not follow be RESTful, perhaps because it’s using an unusual HTTP verb. We adopted a leading underscore to highlight this.

A few things to note:

1. Setting security to sigv4 (also notice the securityDefinitions definitions block at the end) is what allows you to secure your routes with IAM permissions.
2. The definition for OPTIONS should NOT have any security around it. Browsers hit it at their discretion, and there’s no way to add authorization headers to that request. It must be left open.
3. This block allows for multipart form posts. This was something API Gateway didn’t support until recently and is another example of a critical feature you might not realize is available.

x-amazon-apigateway-binary-media-types:
- "multipart/form-data"

4. Finally, the blocks for 4XX and 5XX errors allow errors thrown in the app to pass all the way to the client. Each error thrown in the application is HTTP friendly. (Using the Boom module).

API Gateway Deployment

We have a separate CFT for the API Gateway deployment. It’s so straightforward it isn’t worth discussing. Here’s the template in case you’re curious

Description: ApiGateway API Deployment for ApplicationAPI
Parameters:
ApiId:
Type: String
Description: ApiId for applicationapi-api
Stage:
Type: String
Description: Deployment stage
Resources:
ApplicationAPIDeployment:
Type: "AWS::ApiGateway::Deployment"
Properties:
Description: !Sub Deployment for applicationapi-api (v0.0.1)
RestApiId: !Ref ApiId
StageName: !Ref Stage

IAM permissions

Just a quick word about API permissions. You’ll want to apply the execute-api:Invoke permission to anything needing access to this API. It’ll look something like this:

{
"Action": [
"execute-api:Invoke"
],
"Resource": "arn:aws:execute-api:*:*:apiId/*",
"Effect": "Allow"
}

apiId is where the actual id for your API is supposed to go. You have wide power to lockdown your APIs. The policy aboves grants unrestricted access to the API, but you can lock things down by resource and verb.

Conclusion

Obviously, a few pieces are missing. You’ll need an application the NLB is in front of, and you’ll still need to define all your IAM permissions. But those are elements unique to your application. Hopefully, you can take what you’ve learned here and apply it to your app.

Finally, if there’s anything unclear, please let me know in the comments below. We understand how frustrating it can be to read blog posts missing the critical piece of information you are looking for, and we want to be comprehensive.

--

--