A Golang AWS Lambda service that is attached to an AWS API Gateway and authorises JWT Bearer tokens using RSA256.
This service attaches to an AWS API Gateway to authorize the JWT Bearer token provided an API end-point from a Consumer (something calling the API). Included in this repo is an example API service that serves a couple of endpoints as a demonstration.
Notes:
- This service has been written to be as generic as possible and can be used as a drop-in authorisation step in any application, whether in Go, which is what this is written in, or in another programming language.
- This service is designed to work with RSA-256 encryption; for HMAC and other methods, you'll need to modify the code, or find something else.
The service is attached to an AWS API Gateway service (the Service) that sits in front of the software that processes the API request, in this case, this is written as another Lambda. Doing so in this way means that the authorisation layer is decoupled from the business logic for processing the actual API request, and can thus be developed, altered, and tested in isolation from each other. This keeps the cognitive load for development to a minimum and maintains the domain knowledge solely within the service processing the request and how to authenticate connexions (whose method might change) in a separate space: this one.
The diagram below shows this service in relation to using an API gateway service (the Consumer), such as Kong Gateway, AWS API Gateway, and other AWS services used in a given application.
The following flow chart should summarise the interaction between the Consumer and the Service laid out in more detail below:
The Consumer (which in this example real-world case would be the Kong Gateway, but in local development is the integration and unit tests) has access to a private and public key-pair and uses the private key to sign the JWT and publishes the public key via a service called JWKS. We need the private-public keypair because we are using RSA256 encryption. We could also use HMAC, which simply uses a shared secret, but RSA is more secure, and so we're using that.
The Consumer will have published a list of public keys available at a know URL path (/.well-known/jwks.json). Each entry in the public list is known as a JSON Web Key and the entire published list a JSON Web Key Set. An example entry for an RSA-256 public key is:
{
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"n": "wZLyxWWovHBpQfY01VEkAe6tnsA_rVgHg2Wu13slebRdZxgzxiDSYQXOGbhTiBAWHvrNQ3pUYnp3kLCAVjcKGQ998j3Ls3V9RTYBqG5Wz1RDbrdcVivTH-4WbYH8I7--XtIx9_DAgpgEJnQrraVUMsN0O6g3aBxX7TKpp12Y6dbEliy6Sh9gFg_GrzI2-rJpopW9TkzAV4Tcu-NPTp-Wc7gi9ymAeaSTRarHYJcd38K7DHcGXkISBhofbTiU6k1oGsywkPbbVCJvOOEdTUWQ7o9syHt0Sy2b8rxM9Rtgfx5J3HAK1WlJ6q_unZX4vES7y2491e9CibygmzwOYCuJWQ",
"e": "AQAB",
"kid": "an-rsa-key-id"
}
I created the above using the tool at https://russelldavies.github.io/jwk-creator/. The rather obfuscated parameters are explainable by showing the following part of the Go code:
type JsonWebKey struct {
Algorithm string `json:"alg,omitempty"`
Family string `json:"kty"`
Use string `json:"use"` // set to "sig" as we're using it for signing, not encryption
RsaPublicKeyModulus string `json:"n,omitempty"`
RsaPublicKeyExponent string `json:"e,omitempty"`
KeyId string `json:"kid,omitempty"`
}
The Public key is represented as a pair of values: the exponent and n-modulus. The tool at https://russelldavies.github.io/jwk-creator/ can help you create the JWK for you.
The JSON Web Key Set looks like thus:
{
"previous": [
],
"keys": [
{
"alg": "RS256",
"n": "wZLyxWWovHBpQfY01VEkAe6tnsA_rVgHg2Wu13slebRdZxgzxiDSYQXOGbhTiBAWHvrNQ3pUYnp3kLCAVjcKGQ998j3Ls3V9RTYBqG5Wz1RDbrdcVivTH-4WbYH8I7--XtIx9_DAgpgEJnQrraVUMsN0O6g3aBxX7TKpp12Y6dbEliy6Sh9gFg_GrzI2-rJpopW9TkzAV4Tcu-NPTp-Wc7gi9ymAeaSTRarHYJcd38K7DHcGXkISBhofbTiU6k1oGsywkPbbVCJvOOEdTUWQ7o9syHt0Sy2b8rxM9Rtgfx5J3HAK1WlJ6q_unZX4vES7y2491e9CibygmzwOYCuJWQ",
"kid": "100pWESlO_EEphwIud5RDcpUsgHo0M6_rJlZVZe-Ss0",
"use": "sig",
"e": "AQAB",
"kty": "RSA"
}
]
}
And the Go:
type JsonWebKeySet struct {
Keys []JsonWebKey `json:"keys"`
}
The caller creates a JWT token and sends it in the Authorization header in the HTTP request. The token is in three parts separated by full stops and is prefixed with the word Bearer and a blank space. You can use the tool at https://jwt.io/ to create one using either the private key included in with service or by creating your own one. You can create a public and private key pair by using a tool such as OpenSSL, e.g.:
openssl genrsa 2048 > private.pem
openssl rsa -in private.pem -pubout -out public.pem
Using the aforementioned tool, you will need to set the Issuer and Subject in the payload of the JWT to match those below, or update where the token is in the test code (look for the constant bearerToken) so that the tests continue to pass:
{
"iss": "https://some-url",
"sub": "some_consumer"
}
What are the Issuer (iss) and Subject (sub)? These two are what's known as claims. These are things that the Consumer is claiming about itself to the service it is connecting to. Claims can be a further step of verification, such as whether the user is expected to have admin privileges or not. We could, for example, check in our code that these match some expected values, but here we are simply logging them for the record. The two values in this claim represent:
-
Issuer - Identifies principal that issued the JWT. In other words the service that created the JWT (in our example, the Kong Gateway) - this is where we expect the JWKS to be - https://some-url/.well-known/jwks.json - so by getting to this point in the code, we would have already
-
Subject - Identifies the subject of the JWT. The service (the entrypoint here being the handleRequest function in main.go) receives the HTTP request containing the JWT encoded in the Bearer token of the Authorization header and does the following things:
-
Checks that the Bearer token is present and in the correct format
-
Contacts the JWKS service and looks for a JWK for the RS256 algorithm
-
Recreates an RSA public key from the n-modulus and exponent components in the JWK
-
Verifies the signature by checking against this recreated public key
-
Checks the token can be parsed okay and that the Claims can be extracted
I hope that the code should be fairly self-documenting based on the above knowledge, so I've not gone into more detail here about it.
The service can be built and tested using the Makefile. It can also be built for both local and target processor architectures. This is important because you may have a different CPU architecture to that of AWS. For example make build
builds the service for your local architecture, and make target
for the target AWS architecture.
Tests can be run with make unit-test
or make int-test
or make test
to run all of them. To run the integration tests you will need Docker running. The integration tests covered are in features file.
The service can be deployed from the command line, or in a Jenkins, GitHub Actions, or other CI/CD pipeline using the Go deploy helper tool.
More information about building, testing and deploying is in BUILD.md.
You can use a tool, such as postman to test this service, whose POST requests are wrapped in the JWT Authorizer service. You'll need to set the JWT Bearer token in order to do this.
The parameters are:
- Authorization Type: JWT Bearer
- Add JWT Token to: Request Header
- Algorithm: RS256
- Private Key: load from private.pem
- Payload:
{
"iss": "https://some-url",
"sub": "some_consumer"
}
- Request header prefix: Bearer
- JWT Headers:
{
"alg": "RS256",
"typ": "JWT"
}
The following endpoints are provided.
URL: /v1/health-check
Method: GET
Parameters: None
Auth required: No
Permissions required: None
Data examples: None
Code: 200 OK
Data examples: Not applicable
No error responses defined
URL: /v1/teapot
Method: GET
Parameters: None
Auth required: No
Permissions required: None
Data examples: None
Code: 418 I'm a teapot
Data examples: Not applicable
No error responses defined