Skip to content

mikebharris/JSON-Web-Token-RSA256-Authorization-Service

Repository files navigation

AWS API Gateway RSA256 JWT Bearer Token Authorizer service

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.

How does this work?

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.

JWT Authorizer service in context with other services

The following flow chart should summarise the interaction between the Consumer and the Service laid out in more detail below:

Consumer

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.

Public key publication using JWK and JWKS

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"`
}

Creating the JWT

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.

Building, testing and deploying the service

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.

Using Postman (or similar) to test service

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

Endpoint API specification

The following endpoints are provided.

Healthcheck

URL: /v1/health-check

Method: GET

Parameters: None

Auth required: No

Permissions required: None

Data examples: None

Success Response

Code: 200 OK

Data examples: Not applicable

Error Response

No error responses defined

Teapot

URL: /v1/teapot

Method: GET

Parameters: None

Auth required: No

Permissions required: None

Data examples: None

Success Response

Code: 418 I'm a teapot

Data examples: Not applicable

Error Response

No error responses defined

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published