Skip to content

lmammino/oidc-authorizer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

87 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

oidc-authorizer

Rust codecov Published on SAR: Serverless Application Repository

A high-performance token-based API Gateway authorizer Lambda that can validate OIDC-issued JWT tokens.

The OIDC-Authorizer logo represents a determinate otter dressed like a knight with a sword and a shield. The shield has the API Gateway logo on it.)

🀌 Use case

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.

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.

⚽️ Design goals

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.

πŸš€ Installation

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.

πŸ› οΈ Configuration

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:

JwksUri

  • Environment variable: JWKS_URI
  • Description: The URL of the OIDC provider JWKS (Endpoint providing public keys for verification).
  • Mandatory: Yes

MinRefreshRate

  • 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)

PrincipalIdClaims

  • 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"

DefaultPrincipalId

  • 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"

AcceptedIssuers

  • 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: ""

AcceptedAudiences

  • 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: ""

AcceptedAlgorithms

  • 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: ""

AwsLambdaLogLevel

  • 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"

StackPrefix

  • 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".

πŸ›‘ Validation Flow

The following section describes the steps that are followed to validate a token:

  1. The token is parsed from the Authorization header of the request. It is expected to be in the form Bearer <token>, where <token> needs to be a valid JWT token.
  2. The token is decoded and the header is parsed to extract the kid (key id) and the alg (algorithm) claims. If the kid is not found, the token is rejected. If the alg is not supported, the token is rejected.
  3. 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 the MIN_REFRESH_RATE environment variable.
  4. 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 the nbf (not before) claim. If the token is expired or not yet valid, the token is rejected.
  5. 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.
  6. 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.
  7. 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.

πŸ€‘ Context Enrichment

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}

πŸƒβ€β™‚οΈ Benchmarks

We have benchmarked this authorizer against an equivalent Python implementation. And these are some of the main findings:

  • The Rust version is about 16 times faster than the Python version when it comes to cold starts (~42ms vs ~670ms).
  • Execution times are quite comparable between the two implementations, with the Rust version being only slightly faster. This is probably because the Python library used to do the JWT validation is quite optimized.
  • Memory utilization is about 3.5 times smaller in Rust (22MB vs 77MB). This allows us to use a smaller memory size for the Rust version, which results in a lower cost.
  • The cost per request is about 3 times smaller in Rust compared to Python (~1.44 USD vs ~4.13 USD per every 100Mln invocations).

If you want to have a more detailed look at the benchmark methodology and the results, you can check out the dedicated benchmarking repository.

πŸ™Œ Contributing

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.

πŸ‘¨β€βš–οΈ License

Licensed under MIT License. Β© Luciano Mammino.

πŸ™ Acknowledgements

Big thanks to: