Securing your HTTP endpoints is a must. You hear about "Zero Trust architectures" everywhere. you. turn. In a nutshell, "Zero Trust" means not relying on network topologies to prevent access; you should verify every call. AWS helps make this easy on engineers by providing fully-managed, easy-to-configure services like API Gateway, Cognito, and IAM, that allow us to implement OAuth flows to achieve Zero Trust.
I want to give examples of three ways to set up Zero Trust auth using API Gateway v2: IAM, JWT, and Request (custom). Accompanying each is a working GitHub repository that implements that specific approach. While describing each type of authorization, I will use OAuth terms to describe the actors of each implementation, so let's review their definitions.
Terminology
In common language, we have four roles:
The User
The Device (phone or browser)
The Application
The API
In the OAuth spec, they use a slightly different wording:
Resource Owner (the user)
User Agent (the device)
Client (the application)
Resource Server (the API)
There is another role, the Authorization Server. It manages access to the API it is protecting. The user logs into the Authorization Server, receives a token, and then uses the token to access the API. The API then validates the token without ever having to see the credentials used to generate it.
I'll refer to these roles as we examine how we can secure our API endpoints for a “Client Credentials Flow.” That is, where the user and the API are both machines (a.k.a. “Machine to Machine” or M2M).
The AWS HTTP API
AWS offers two flavors of API Gateway: REST and HTTP. While they both perform many similar operations, I will focus on the HTTP (or v2) version of API Gateway because of its ease of use regarding authorization.
The AWS HTTP in API Gateway supports three types of endpoint authorization out of the box: JWT, Simple/Custom, and IAM. Each has some strengths and costs that are worth considering. I'll start with the easiest to implement: IAM authorization.
IAM Authorization
Note - this is not truly "OAuth," but the particular terms still work if you squint a little.
Let's imagine you have an API whose clients are "internal." That is, your clients are services that your team owns or perhaps that your company owns. In this situation, you can elect to protect your API with AWS IAM as the (sort of) Authorization Server. In the serverless framework, you need only to decorate your function with an authorizer of type aws_iam
like so:
Then, your client uses a signing protocol (AWS Signature v4) to sign each request. This both verifies the caller's identity and prevents tampering with the request after it has been sent. In my code examples, I am using the aws4-axios library to sign calls to my IAM-protected service automatically.
By choosing IAM Authorization, you allow any caller with AWS credentials who has an execute-api
policy defined for your API resource to hit it. For example, if I have a service (the client) that needs to talk to your API (the resource server), I would add a policy to my role similar to the following:
{
Effect: Allow
Action: execute-api:Invoke
Resource: arn:aws:execute-api:us-east-1:12345678:your-api-id/stage/GET/hello
}
This policy says that I can "execute" the API with the id/stage/method/route listed in the Resource
section. This allows anyone who can create an IAM policy in the AWS account to be able to access the API. No notification from the client is required, only access to create policies on the account.
This is not quite Zero Trust; this is "somewhat trust." The whole idea behind Zero Trust is to move away from the network-layer protection of a VPC, where every API inside is open. When using IAM protection, you shift from "anyone running inside the network" to "anyone who can deploy an IAM policy to the network." This may be a fine fit for some situations, but it does have some downsides.
Pros
Easy to implement on API
Easy to implement on Client
Anyone in the AWS account can use your API
Cons
Anyone in the AWS account can use your API
No control over which clients are using your API; if they can deploy to your AWS account, they can give themselves access.
No way to revoke or prevent a client from using your API at the API authorization layer (although you can do this in your application logic if you really want)
JWT Authorization
This approach to securing your API is a more direct OAuth "Client Credentials Flow" implementation. The entities involved are the Client, the Server (your API), and the Authorization Server. The Authorization server is commonly a Cognito User Pool that the owners of the API configure and maintain, but you can also configure an OAuth-compatible authorization service like Auth0.
In this scenario, let's imagine a 3rd-party client wishes to use your API. As the resource owner, you configure the Authorization Server to create a new App Client, then take the App Client's id & secret and hand them to the new client. You also let your API know that "any requests from this App Client are allowed." From here, we follow the standard OAuth Client Credentials Flow.
Let's outline this flow:
The client contacts the Authorization Server and presents its credentials (id & secret).
The Authorization Server then returns an access token to the client.
The client uses that access token on each request to the API.
The API will automatically validate the signature on the access token and ensure the client id is allowed.
With the AWS HTTP API, no authorization function is necessary, and the API owner does not need to write any authorization code at all. As long as the user is correctly submitting the access token, and it is from the trusted issuer, it will perform all necessary validation. Be sure to keep your serverless.yml
file up to date with your issuer and your audience(s) (below).
Pros
Full control over which clients are allowed to use your API
Fully no trust; callers can be from the same account or from outside your business
No need to write any token validation code; leverages standard JWT structure
Cons
Must configure an external OAuth Server (Cognito, Auth0, etc.)
Adding a new client requires a redeployment of the API
Only works with JWTs
Request (Custom) Authorization
The last option is a traditional1 custom authorizer that API Gateway has supported for years in its REST API (v1) with a nice twist which I will explain below. In the serverless framework, declare a function to act as your authorizer and set it as a customAuthorizer
in the provider
section of your serverless.yml
file (below).
It is called a "custom" authorizer because you, the Resource Server, get to decide how you validate and authorize access. For example (and only for an example), I built a custom authorizer that will validate you if your token is parsable to a number; otherwise, your request is denied. You can do anything you want - it's your authorizer.
Why would you choose this approach? I once had to build a public-facing API that would receive calls from a legacy Web client and new machine-to-machine 3rd-party users. This meant I had to accept either an access token or an id token2. The custom authorizer first tried to validate the id token against a 3rd-party issuer and then, if that failed, tried to validate against our Cognito issuer. The only way to pull this off was with a custom authorizer.
One note (the "twist") is that HTTP APIs (v2) support a "simple" mode that greatly reduces the cognitive overhead of creating and maintaining these custom authorizers. Instead of returning an IAM policy statement, you can return a plain object with clear indicators of whether the call is authorized:
{
isAuthorized: true, // boolean
context: { ... }, // object
}
If you've had to create the REST API (v1) custom authorizer response before, you can appreciate what a tremendous improvement this is.
Pros
Full control over how you authorize; go nuts
Works with any type of "token" you care to handle
Fully no-trust; callers can be from the same account or from outside your business
Can handle complex cases (multiple issuers or token types)
Cons
You have to write your authorizer by hand3
Must configure and/or interface with an external OAuth Server issuer
Wrap-Up
HTTP API (API Gateway v2) gives us three easy-to-implement ways to lock down our authorization and achieve Zero Trust. Know the trade-offs between them so you can make the best choice for your team's situation. In general, I use IAM authorization for my internal services and some form of JWT for my external-facing services. Have fun!
Further Reading
If anything in serverless can be called “traditional”
Yes, I know - not very OAuth. It was a stop-gap measure.
“Code is a liability. Keep it to a minimum.” Serverless Manifesto