In my previous Serverless post I covered creating a basic “Hello World” app with a SAM template and a Lambda function written in Python. In this post we will extend the SAM template using a Swagger definition and enable a custom authorizer on our endpoint.
Extending with Swagger
Here is the SAM template from the previous post:
Here, our API Gateway resource is implicitly defined by way of the GetApi event on our Lambda function. This tells CloudFormation that an API Gateway with a path of “/” and the GET method must be created to execute the Lambda function.
The API Gateway supports a lot of features and functionality, but much of it is not a part of the SAM spec. In order to extend our API Gateway beyond basic Lambda functions executed upon requests to endpoints, we will need to define an AWS::Serverless::Api resource and provide a Swagger definition. Within that Swagger definition we will be able to extend our API.
Here is our application above with the API resource added:
Let’s focus in on the new API resource.
Before, our API Gateway was being created with the default Stage and Prod stages. Now that we are defining the resource we must provide the stage name. Otherwise, the rest of this object is the Swagger definition.
There are two ways of setting the Swagger definition. Inline Swagger definitions use the DefinitionBody key as shown above. To point to the location of a Swagger definition file you can use the DefinitionUri key. This key works just like the CodeUri key for our Lambda function.
The info:title key is being set to the name of the CloudFormation stack we deploy our function to. The keys under path should all be the path of the API Gateway endpoint that is being defined, and each key for the path should be an HTTP method. We only have one path, “/”, and it only supports one method, GET.
Here we encounter our first of AWS’s Swagger Extensions for API Gateway. x-amazon-apigateway-integration allows API Gateway to integrate with different backends including HTTP services, SNS, SQS, and Lambda. The type: aws_proxy is specifically for Lambda functions. You may be confused about the httpMethod: post here as our endpoint is set to use the GET method. This is specifically for API Gateway invoking the Lambda function and does not involve the HTTP request.
Then we have the Uri to the backend integration:
The Fn::Sub function is a CloudFront template function for replacing values within a string. This ARN (an AWS resource identifier) populates the Region our stack is running in, and the ARN of our Lambda function that is also defined in our template.
Which, we have only made one change to our Lambda resource:
Under the event, we have added a reference (Ref) to our API Gateway resource. Ref is another CloudFormation function that returns the value of an item (in this case, our API Gateway resource returns it’s ID when referenced).
At this point, you may notice that we are now doubling up on how we define our API. Not only do we need to define all of our endpoints in the Swagger definition, but we must still write our Lambda functions in full. While not ideal, it is necessary as we move into advanced integrations between API Gateway and Lambda.
A Custom Authorizer is a Lambda function attached to an API Gateway endpoint. This Lambda function is invoked when a request to that endpoint is made. Based upon authorization tokens, headers, query strings, stage variables, or context parameters. Once the request is evaluated, it is either denied or the Lambda function will return an IAM policy granting the request. It is then allowed through as normal.
You can only define a custom authorizer in your SAM template by using an AWS Swagger extension. Here is our updated SAM template with the custom authorizer included:
The Swagger definition has been expanded and we have added another Lambda function, but this time with a Lambda Permission resource.
The securityDefinition key is where we define our authentication methods for our API. We are using two additional AWS Swagger extensions to define the custom authorizer: x-amazon-apigateway-authtype and x-amazon-apigateway-authorizer. The former is always custom for a custom authorizer.
The the example authorizer in my example has a static token that it validates. so we specify the type as token. The authorizerUri is the same style ARN as above with the x-amazon-apigateway-integration, but using the ARN of our authorizer’s Lambda function. The authorizerResultTtlInSeconds if the amount of time the resulting IAM policy is cached. More on that in a bit.
Our definition of the authorizer’s Lambda function is bare bones. There are no events that trigger it as a part of the SAM spec – that is defined in our Swagger definition. Because we have no events, we must create the permissions resource that allows API Gateway to invoke this function.
Here is the Python code for the authorizer function:
The Custom Authorizer only needs to perform two tasks: 1) validate the request by the chosen method (in our example: the Authorization header’s value which must match ‘Bearer a.b.c’), and 2) responds with an object containing a principalId and an IAM policy.
The principalId comes into play with the authorizerResultTtlInSeconds value above. API Gateway will cache the result of an authorization for an amount of time equal to this value. If the origin repeats a request, and they have already authenticated, they will be allowed through without invoking the custom authorizer again! In my example I am passing the Authorization header’s value as the principal ID.
The IAM policy is a document that defines permissions for the request. You can specify actions to ALLOW and actions to DENY for a given Resource (in this case, granting execute-api:Invoke for the HTTP endpoint that was requested).
In my example, a successful authentication generates the following response to API Gateway:
Our project directory now has the following structure:
We can update the application stack deployed in the previous post by re-running our package and deploy commands on the same stack name. Alternatively, you can deploy to an entirely new stack using the same commands.
Now if we attempt to call our API we will receive an error message telling us we are unauthorized. Reattempt with the Authorization header in place and we get a successful response!
Now that we have covered how to extend our serverless application using a Swagger definition and secured it with a Custom Authorizer, we can expand into having an S3 bucket provide back-end storage and use API Gateway’s binary data support to POST and GET files!
You can find the templates and code shown in this post here on my GitHub: