Debugging custom ApiGateway authorizers

By admin

Last week
DATE

, while working on a custom REST API

Gateway Lambda
PRODUCT

authorizer, I spent some time trying to debug a mysterious

500
CARDINAL

error with a {"message":null} body. In this article, I will share why this was happening and how to debug this kind of error when building custom

API Gateway
FAC

authorizers.

The use case

Before diving into the problem, let me give you a bit of context about the use case I was working on. As I said, I was working on an open-source project implementing a custom authorizer for

AWS API Gateway
ORG

. The project is simply called oidc-authorizer and it’s already available on

GitHub
ORG

and

the Serverless Application Repository
ORG

(SAR). This authorizer is implemented as a

Lambda
NORP

function and it allows you to authenticate requests following to OIDC (OpenID Connect) protocol.

To better understand what that means, let’s have a look at this lovely diagram (that I am proud to have drawn by myself):

In this diagram, we see a user that sends an authenticated request to

API Gateway
ORG

. API Gateway is configured to use a lambda as a custom authorizer. The lambda talks with a given OIDC provider to get the public key to validate the user token and responds to

API Gateway
FAC

to Allow or Deny the request.

This type of authentication is based on the idea that a token (in this case, a

JWT
ORG

) can be trusted only if it’s signed by an authority we trust. That’s why we need to talk with the OIDC provider to get the public key to validate the signature of the token.

I have been speaking and writing at length about

JWT
ORG

before, so I am just going to say that if you want to deep dive into the fascinating topic of OIDC you can check out the official specification of the OIDC standard. If you have the guts to read protocol specifications, it’s a very interesting read, I promise! 😇

The problem

Ok, all pretty cool, but what was the problem?

The problem is that I am a terrible programmer and when I started testing my

first
ORDINAL

version of the authorizer my code was quite buggy and it didn’t work in the way that

REST API Gateway Authorizers
PRODUCT

are supposed to work.

I take responsibility for that, but that’s only part of the problem. The other side of the coin is that

AWS
ORG

wasn’t really giving me a useful error message: the only thing I could see when making a request was a

500
CARDINAL

error with a response body containing only the JSON payload {"message":null} .

The full response:

HTTP/2

500
CARDINAL

content-type: application/json content-length:

16
CARDINAL

date: Sat,

04 Nov 2023
DATE


12:13:50 GMT
TIME

x-amzn-requestid: […] x-amzn-errortype: AuthorizerConfigurationException x-amz-apigw-id: w218poymg7 x-cache: Error from

cloudfront
ORG

via:

1.1
CARDINAL

[…]cloudfront.net (

CloudFront
ORG

) x-amz-cf-pop: […] x-amz-cf-id: […] {"message":null}


Shrugh
PERSON

… What do you do in these cases? 🤷

You try to look for some logs in

CloudWatch
GPE

, right? Well, I did that and I found nothing.

API Gateway
ORG

doesn’t have a log group by default and I was expecting to see some logs from my

Lambda
NORP

function, but there was nothing useful there!

The solution

The solution is to enable extensive logging for

API Gateway
FAC

.

So, how do we do that?

We can do that using

Infrastructure
ORG

as code using

CloudFormation
PRODUCT

or SAM.

These are the logical steps we need to follow:

Create a Role that allows

API Gateway
FAC

(at the service level) to write logs to

CloudWatch Use
ORG

the AWS::ApiGateway::Account resource to assign the role we just created to

API Gateway
FAC

. Updated the specific instance of

a REST API Gateway
PRODUCT

stage to enable logging and tracing on it.

Enabling API Gateway logging to

CloudWatch with CloudFormation
ORG

OK, let’s start with the code for the

first
ORDINAL


2
CARDINAL

steps using the following

CloudFormation
ORG

template:

AWSTemplateFormatVersion : "

2010-09-09
DATE

" Description : Allow API Gateway to write logs to

CloudWatch Resources
ORG

: ApiCWLRoleArn : Type : AWS : :

ApiGateway
ORG

: : Account Properties : CloudWatchRoleArn : !GetAtt CloudWatchRole.Arn CloudWatchRole : Type : AWS : : IAM : : Role Properties : AssumeRolePolicyDocument : Version : "

2012-10-17
DATE

" Statement : Action : "sts:AssumeRole" Effect : Allow Principal : Service : apigateway.amazonaws.com Path : / ManagedPolicyArns : – "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"

To avoid overwriting other roles, we should only have

one
CARDINAL

AWS::ApiGateway::Account resource per region per account, so we are creating a generic stack only to allow

API Gateway
FAC

to send logs to

CloudWatch
ORG

. We will be defining our APIs in other stacks.

Let’s say we call this file apigw-logging.yaml . We can now deploy it with the following command:

aws cloudformation deploy \ –template-file apigw-logging.yaml \ –stack-name apigw-logging \ –capabilities CAPABILITY_IAM

Make sure to specify a region either with the –region flag or by setting the

AWS_DEFAULT_REGION
PERSON

environment variable.

If all goes well, you should see an output like this:

Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack – apigw-logging

Note: if you prefer to use

SAM
ORG

(which we will use later), you can use it to deploy the template above with the following command:

sam deploy –guided –template apigw-logging.yaml
PERSON

.

Enable logging and tracing for a specific

API Gateway
FAC

stage using

SAM
PRODUCT

Now that we have a role that allows

API Gateway
FAC

to write logs to

CloudWatch
ORG

, we can enable logging and tracing for a specific

API Gateway
FAC

stage.

This time, to make our life a bit easier, we prefer to use

SAM
ORG

(rather than CloudFormation) to define our

API Gateway
FAC

.

SAM
ORG

is a superset of

CloudFormation
ORG

that allows us to define serverless applications more concisely.

So, let’s say we have another

SAM
ORG

template that defines an API Gateway and the stage we want to enable logging. The template could look like this:

AWSTemplateFormatVersion : "

2010-09-09
DATE

" Transform : AWS : : Serverless – Description : AWS SAM template with a simple API definition Resources :

ApiGatewayApi
CARDINAL

: Type : AWS : : Serverless : : Api Properties : StageName : prod Description : Our production API TracingEnabled : true

MethodSettings
PERSON

: –

HttpMethod
ORG

: "*"

LoggingLevel
ORG

: INFO ResourcePath : "/*" MetricsEnabled : true DataTraceEnabled : true SampleApiFunction1 : Type : AWS : : Serverless : : Function Properties : Events : ApiEvent : Type : Api Properties : Path : /hello Method : get RestApiId : Ref :

ApiGatewayApi
CARDINAL

Runtime : python3.9 Handler : index.handler InlineCode : | def handler(event, context): return {‘body’: ‘Hello, my friend!’, ‘statusCode’:

200
CARDINAL

} Outputs : HelloEndpoint : Description : "API Gateway endpoint" Value : !Sub "https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/prod/hello"

Let’s deploy this stack with:


sam deploy
PERSON

–guided –template api.yaml

Note that

SAM
ORG

will warn us that we haven’t set any authorization for our API Gateway. That’s fine for now, we will add it later. But you need to make sure you reply Y to the following question:

SampleApiFunction1 has no authentication. Is this okay?

If all goes well, you should see an output containing the URL of our endpoint. If you call it you should see the message: “Hello, my friend!”.

Adding a custom authorizer to our API Gateway

Now that we have

an API Gateway
FAC

with a stage that has logging and tracing enabled, we can add a custom authorizer to it.

The goal of this article is not to deep dive into how to write a custom

API Gateway
FAC

authorizer. If you want a deep dive into that, I warmly recommend this fantastic article by

Alex DeBrie
PERSON

: “

The Complete Guide to Custom Authorizers
WORK_OF_ART

with

AWS Lambda
ORG

and API Gateway”.

Just to have a simple example to work with, let’s say that we want to authorise all requests that provide an

Authorization
ORG

header that contains an odd number.

So the following headers will be authorized: ✅

Authorization: 1 Authorization:

3
CARDINAL

Authorization:

5
CARDINAL

While, the following headers will be denied:

Authorization
ORG

:

2
CARDINAL

Authorization:

4
CARDINAL

… Authorization: something else

Even failing to pass the

Authorization
ORG

header will result in a denial.

The code for our custom

Authorizer
PERSON

could look like this (don’t judge my terrible Python skillz, pleaze 😅):

def handler ( event , context ) : authorization_token = event [ ‘authorizationToken’ ] endpoint = event [ ‘methodArn’ ] try : if int ( authorization_token ) %

2
CARDINAL

==

1
CARDINAL

: return { "principalId" : "authenticatedUser" , "policyDocument" : { "Version" : "

2012-10-17
DATE

" , "Statement" : [ { "Action" : "execute-api:Invoke" , "Effect" : "Allow" , "Resource" : "*" } ] } } except : pass return { "policyDocument" : { "Version" : "

2012-10-17
DATE

" , "Statement" : [ { "Action" : "execute-api:Invoke" , "Effect" : "Deny" , "Resource" :

str
PERSON

( endpoint ) } ] , } }

If you save this Python code in a file called

handler.py
ORG

in the same folder where we have our api.yaml template, then we can add the authorizer to the template like this:

Resources : SimpleApiAuthorizerLambda : Type : AWS : : Serverless : : Function Properties : Runtime : python3.9 Handler : handler.handler CodeUri : .

This only creates the

Lambda
ORG

resource, but to configure it as an authorizer, we need to add the following section to the

ApiGatewayApi
CARDINAL

resource:

Resources :

ApiGatewayApi
CARDINAL

: Type : AWS : : Serverless : : Api Properties : Auth :

DefaultAuthorizer
PERSON

:

SimpleApiAuthorizer Authorizers
PERSON

:

SimpleApiAuthorizer
PERSON

: FunctionPayloadType : REQUEST

FunctionArn
ORG

: !

GetAtt SimpleApiAuthorizerLambda
PERSON

.

Arn

PERSON

Ok, we are finally ready to deploy our updates to the api.yaml template with:


sam deploy
PERSON

–guided –template api.yaml

If all went well, we should now be ready to send requests to our

API Gateway
FAC

and see the authorizer in action.

Let’s try to send a request with a valid authorization header:

curl -i -X GET -H ‘Authorization:

17
CARDINAL

‘ < your-api-url > /hello

You should see a response like this:

HTTP/2

200
CARDINAL

content-type: application/json content-length:

21
CARDINAL

date:

Sun
ORG

, 05

Nov 2023
DATE

13:24:15 GMT x-amzn-requestid: 7a8c5040-e76b-400f-8407-0c6ed9b72e46 x-amz-apigw-id:

N7Sb5GwpDoEEf9A=
CARDINAL

x-amzn-trace-id: Root=1-6547977f-2ec69acd4fd64cb0285e36d7 x-cache: Miss from

cloudfront
ORG

via:

1.1 33388636a7cb2afa812b276d900f88d4.cloudfront.net
CARDINAL

(

CloudFront
ORG

) x-amz-cf-pop: DUB56-P1 x-amz-cf-id: DOa6otRm_gZyYG0rn-FM-vC1HhjzuhJIAzfms02bbny_0DpmKMTc3w== Hello, my friend!

If instead we send a request without an

Authorization
ORG

header we should see something like:

HTTP/2

401
CARDINAL

content-type: application/json content-length:

26
CARDINAL

date:

Sun
ORG

, 05

Nov 2023
DATE

13:25:29 GMT x-amzn-requestid:

e9ab5f97
ORG

-9a5a-4f34-94d7-87df74a235de x-amzn-errortype:

UnauthorizedException
ORG

x-amz-apigw-id: N7SnfFYZjoEEolg= x-amzn-trace-id: Root=1-654797c9-3cd1a8f5095fef3e3297ac26 x-cache: Error from

cloudfront
ORG

via:

1.1 93951ac7649a5f7c158d327385b2aeb8.cloudfront.net
CARDINAL

(

CloudFront
ORG

) x-amz-cf-pop: DUB56-P1 x-amz-cf-id: PuyPcm7sY13bfhigJXgNGTgYkT5CjinaK_uD6bl0jDNdGn_U0_pmHw== {"message":"Unauthorized"}

Finally, if we send an invalid value for the

Authorization
ORG

header (e.g. not a number or an even number) we should see something like this:

HTTP/2

403
CARDINAL

content-type: application/json content-length:

82
CARDINAL

date:

Sun
ORG

,

05 Nov 2023
DATE


13:26:18
TIME

GMT x-amzn-requestid:

49a6b913-a965
CARDINAL

-4f66-a9e1-7b4ccf3b96cc x-amzn-errortype: AccessDeniedException x-amz-apigw-id:

N7SvJHWQDoEEXeA= x-amzn-trace
PERSON

-id: Root=1-654797fa-6ae95da234f15ba6587fb36d x-cache: Error from

cloudfront
ORG

via:

1.1 4b0861a8035fd11b1a90183c566020e2.cloudfront.net
CARDINAL

(

CloudFront
ORG

) x-amz-cf-pop: DUB56-P1 x-amz-cf-id: gA-hS_7NeRjCtmKPc2P76uGyxaDNmWpE8iUZQMsxMHBoWm2W4uTPqw== {"

Message":"User
ORG

is not authorized to access this resource with an explicit deny"}

Note how there’s a difference in the error response between not sending an

Authorization
ORG

header and sending an invalid one. In the

first
ORDINAL

case, we get a

401
CARDINAL

with a {"message":"Unauthorized"} body, while in the

second
ORDINAL

case, we get a

403
CARDINAL

with a {"

Message":"User
ORG

is not authorized to access this resource with an explicit deny"} body.

In the

first
ORDINAL

case,

API Gateway
ORG

is automatically handling the response for us and our custom authorizer lambda is not even invoked. In the

second
ORDINAL

case, the authorizer is invoked and it returns a Deny response, which produces a

403
CARDINAL

error and that specific error message.

Looking at

the API Gateway
FAC

logs

Ok, I have been kind enough to give you a working authorizer, so we didn’t really have to debug much… but we should still have a look at

the API Gateway
FAC

logs to see what they look like and why they could be really useful in case something went wrong and we needed to troubleshoot.

To look at the logs, we need to go to the

CloudWatch
ORG

console. From there, we need to select the Logs section and then the

Log
ORG

groups section. We should see a list of log groups and we should be able to find the one for our API Gateway. It should be called something like

API-Gateway-Execution-Logs_<your-api-id>/prod
PRODUCT

.

As you can see, in these logs you can see quite a bit of detail about all the steps that

API Gateway
FAC

is performing before and after invoking our custom authorizer. This is really useful to understand what’s going on and where the problem could be.

Conclusion

And this brings us to the end of this article!

I hope you enjoyed it and that you learned something new.

If you have any questions or feedback, please let me know in the comments below or on X, formerly

Twitter
ORG

.