A high-performance token-based API Gateway authorizer Lambda that can validate OIDC-issued JWT tokens.
This project provides an easy-to-install AWS Lambda function that can be used as a custom authorizer for AWS API Gateway. This authorizer can validate OIDC-issued JWT tokens and it can be used to secure your API endpoints using your OIDC provider of choice (e.g. Apple, Auth0, AWS Cognito, Azure AD / Microsoft Entra ID, Facebook, GitLab, Google, Keycloak, LinkedIn, Okta, Salesforce, Twitch, etc.).
A diagram illustrating how this project can be integrated.
A user sends an authenticated request to API Gateway. API Gateway is configured to use a custom lambda as an authorizer (THIS PROJECT!). The lambda talks with your OIDC provider to get the public key to validate the user token and responds to API Gateway to Allow or Deny the request.
API Gateway currently exists in 2 flavours: HTTP APIs and REST APIs. As of today, only HTTP APIs implement a built-in JWT authorizer that supports OIDC-issued tokens.
You might want to consider using this project in the following cases:
- You are using REST APIs and you want to secure your endpoints using OIDC-issued tokens. For instance, if you want to build APIs that are only available in a private VPC, you are currently forced to use REST APIs.
- You are using HTTP APIs but your OIDC provider gives you tokens that are not signed with the RSA algorithm (currently the only one supported by the built-in JWT authorizer).
- You want more flexibility in the validation process of your tokens. For instance, you might want to validate the
aud
claim of your tokens against a list of values, instead of a single value (which is the only option available with the built-in JWT authorizer). - You want to customise the validation process even further. In this case, you can fork this project and customise the validation logic to your needs.
This custom Lambda Authorizer is designed to be easy to install and configure, cheap, highly performant, and memory-efficient. It is currently written in Rust, which is currently the fastest lambda Runtime in terms of cold start and it produces binaries that can provide best-in-class execution performance and a low memory footprint. Rust makes it also easy to compile the Authorizer Lambda for ARM, which helps even further with performance and cost. Ideally this Lambda, should provide minimal cost, even when used to protect Lambda functions that are invoked very frequently.
This project is meant to be integrated into existing applications (after all, an authorizer is useless without an API).
Different deployment options are available. Check out the deployment docs for an extensive explanation of all the possible approaches.
Alternatively, you can also consult some of the quick examples in the examples
folder
If you prefer, you can also learn how to host your own SAR application.
The authorizer needs to be configured to be adapted to your needs and to be able to communicate with your OIDC provider of choice.
Here's a list of the configuration options that are supported:
- Environment variable:
JWKS_URI
- Description: The URL of the OIDC provider JWKS (Endpoint providing public keys for verification).
- Mandatory: Yes
- Environment variable:
MIN_REFRESH_RATE
- Description: The minimum number of seconds to wait before keys are refreshed when the given key is not found.
- Mandatory: No
- Default value:
"900"
(15 minutes)
- Environment variable:
PRINCIPAL_ID_CLAIMS
- Description: A comma-separated list of claims defining the token fields that should be used to determine the principal Id from the token. The fields will be tested in order. If there's no match the value specified in the
DefaultPrincipalId
parameter will be used. - Mandatory: No
- Default value:
"preferred_username, sub"
- Environment variable:
DEFAULT_PRINCIPAL_ID
- Description: A fallback value for the Principal ID to be used when a principal ID claim is not found in the token.
- Mandatory: No
- Default value:
"unknown"
- Environment variable:
ACCEPTED_ISSUERS
- Description: A comma-separated list of accepted values for the
iss
claim. If one of the provided values matches, the token issuer is considered valid. If left empty, any issuer will be accepted. - Mandatory: No
- Default value:
""
- Environment variable:
ACCEPTED_AUDIENCES
- Description: A comma-separated list of accepted values for the
aud
claim. If one of the provided values matches, the token audience is considered valid. If left empty, any issuer audience be accepted. - Mandatory: No
- Default value:
""
- Environment variable:
ACCEPTED_ALGORITHMS
- Description: A comma-separated list of accepted signing algorithms. If one of the provided values matches, the token signing algorithm is considered valid. If left empty, any supported token signing algorithm is accepted. Supported values:
ES256
,ES384
,RS256
,RS384
,PS256
,PS384
,PS512
,RS512
,EdDSA
- Mandatory: No
- Default value:
""
- Environment variable:
AWS_LAMBDA_LOG_LEVEL
- Description: The log level used when executing the authorizer lambda. You can set it to DEBUG to make it very verbose if you need more information
to troubleshoot an issue. In general, you should not change this, because if you produce more logs than necessary that might have an impact on cost.
Allowed values:
TRACE
,DEBUG
,INFO
,WARN
,ERROR
. - Mandatory: No
- Default value:
"INFO"
- Environment variable: N/A (only applies to CloudFormation deployments through SAR)
- Description: A prefix to be used for exported outputs. Useful if you need to deploy this stack multiple times in the same account.
- Mandatory: No
- Default value:
""
For instance, If you set this parameter to "Test"
, the ARN of the deployed authorizer when using SAR will be exported as "TestOidcAuthorizerArn"
.
The following section describes the steps that are followed to validate a token:
- The token is parsed from the
Authorization
header of the request. It is expected to be in the formBearer <token>
, where<token>
needs to be a valid JWT token. - The token is decoded and the header is parsed to extract the
kid
(key id) and thealg
(algorithm) claims. If thekid
is not found, the token is rejected. If thealg
is not supported, the token is rejected. - The
kid
is used to look up the public key in the JWKS (JSON Web Key Set) provided by the OIDC provider. If the key is not found, the key is refreshed and the lookup is retried. If the key is still not found, the token is rejected. The JWKS cache is optimistic, it does not automatically refresh keys unless a lookup fails. It also does not auto-refresh keys too often (to avoid unnecessary calls to the JWKS endpoint). You can configure the minimum refresh rate (in seconds) using theMIN_REFRESH_RATE
environment variable. - The token is decoded and validated using the public key. If the validation fails, the token is rejected. This validation also checks the
exp
(expiration time) claim and thenbf
(not before) claim. If the token is expired or not yet valid, the token is rejected. - The
iss
(issuer) claim is checked against the list of accepted issuers. If the issuer is not found in the list, the token is rejected. If the accept list is empty, any issuer is accepted. If the token contains multiple issuers (array of strings), this check will make sure that at least one of the issuers in the token matches the provided list of accepted issuers. - The
aud
(audience) claim is checked against the list of accepted audiences. If the audience is not found in the list, the token is rejected. If the list is empty, any audience is accepted. If the token contains multiple audiences (array of strings), this check will make sure that at least one of the audiences in the token matches the provided list of accepted audiences. - If all these checks are passed, the token is considered valid and the request is allowed to proceed. The principal ID is extracted from the token using the list of principal ID claims. If no principal ID claim is found, the default principal ID is used.
The authorizer enriches the context of the request with the following values:
principalId
: the principal ID extracted from the token.jwtClaims
: a JSON string containing the entire token payload (claims).
These values are injected into the context of the request and can be used to enrich your logging, tracing or to implement app-level authentication.
When you use the Lambda-proxy integration these values are made available under event.requestContext.authorizer
.
For example, this is how you can access the principalId
and jwtClaims
values in a Lambda function written in Python:
import json
def handler(event, context):
print('principalId: ')
print(event['requestContext']['authorizer']['principalId'])
print('jwtClaims: ')
jwtClaims = json.loads(event['requestContext']['authorizer']['jwtClaims'])
print(jwtClaims)
return {'body': 'Hello', 'statusCode': 200}
Proper benchmarks are yet to be written (SORRY π), but for now, to prove that this Lambda is still reasonably fast, here's some data observed during some manual tests (128 MB Memory deployment):
- Cold start times: ~48ms
- Cold start requests (including fetching JWKS from Azure): 120-300ms
- Warm requests (with JWKS in cache): ~10ms
- Actual memory consumption: ~19 MB
Everyone is very welcome to contribute to this project. You can contribute just by submitting bugs or suggesting improvements by opening an issue on GitHub.
Licensed under MIT License. Β© Luciano Mammino.
Big thanks to:
- @Lodewyk11 & @gsingh1 for writing the original Python implementation that inspired this work.
- @eoinsha for suggesting various ways to package and distribute this project.
- @allevo for tons of great Rust suggestions.
- @alexdebrie for his amazing article on custom ApiGateway Authorizers.