This is a Serverless Application Model (SAM) application that provides a Lambda function for signing and issuing JSON Web Tokens (JWTs) with an asymmetric key stored in either AWS Systems Manager Parameter Store or AWS Key Management Service (KMS) using the ECDSA_SHA_256 (ES256) signing algorithm.
It's designed for easy use with Hotsock, but can securely issue JWTs for anything.
This service does not provide functionality for token verification. Instead, the public key is provided in the stack output, which can be used to verify tokens by any external service.
There are two configuration modes for key custody: Parameter Store and KMS. The API is identical for both modes, so there are no application-level design considerations for mode selection.
With this mode, your private key material is generated during installation and stored in Parameter Store as a SecureString
. Its value is encrypted with KMS using the default AWS managed key.
When the JWT Issuer Lambda function (cold) starts, it loads the private key value into memory from Parameter Store and uses it to sign keys for the lifetime of that Lambda execution environment.
You'll grant your internal applications access to invoke this Lambda function and receive signed JWTs without granting them access to the stored private key.
This mode is fast, cost-effective, and secure enough for most cases. Why "secure enough"? It's possible that the private key value could be leaked, modified, or deleted. Whether a bug in code, a bad actor in your AWS account, or a permissions mis-configuration, there are no service-level guarantees on the privacy and integrity of the stored key.
Each JWT signing operation requires a call to invoke Lambda. Each Lambda function invocation to sign a JWT takes less than 2ms. Cold-start invocations take about 175ms.
There are no baseline costs when standing up a stack in Parameter Store mode. Everying is usage-based.
Monthly Cost assuming 1,000,000 signed tokens (us-west-2 pricing example):
- $0.20: Lambda requests ($0.20 per 1M requests)
- $0.0034: Lambda duration ($0.0000000017 per 1ms)
- KMS (decrypt) is called once for each Lambda cold start ($0.03 per 10,000 KMS requests). Actual cold start count is very workload dependent so your mileage may vary, but for a real-world instance of this function serving 400 million invocations per month, the KMS decrypt bill is less than $10 per month.
KMS mode provides additional security. The private key material never leaves the KMS service in your AWS account, ensuring only AWS principals explicitly authorized with kms:Sign
permissions for this key can ever generate digital signatures with this key. Even with this permission granted, no one can ever access the underlying private key. A KMS customer managed key (CMK) is created during stack installation and is used for all signing requests.
Since each JWT must be signed and the private key is not directly accessible, each Lambda invocation must call KMS. This adds some runtime latency for each signing operation and KMS calls incur additional costs.
KMS key material can never be modified and if a key is deleted, there is a deletion recovery period to ensure accidental deletion is not permanent. If your company or organization has key compliance requirements, this is probably the best option for you.
Each JWT signing operation requires a call to Lambda, which calls KMS to generate a token signature. Each function invocation takes ~15ms in Lambda. Cold-start invocations take about 200ms. KMS has a default quota of 300 requests per second for ECC signing operations, so be sure to request an increase if you need more than that.
Standing up a stack in your AWS account creates a KMS key, which incurs a charge for its ongoing management. Other than the key management, everything is usage-based.
Monthly Cost assuming 1,000,000 signed tokens (us-west-2 pricing example):
- $1.00: KMS key management
- $15.00: 1,000,000 KMS asymmetric signing requests ($0.15 per 10,000 requests)
- $0.20: Lambda requests ($0.20 per 1M requests)
- $0.13: Lambda duration ($0.0000000067 per 1ms)
As you can see, most of the cost is in KMS. If you're signing a billion tokens each month, this might become cost prohibitive.
Launch a stack in your AWS account in less than 5 minutes. Installs using CloudFormation to any of the following regions.
The only option you need to consider is the KeyCustodianParameter
. Choose ParameterStore
or KMS
based on your assessment above, compliance requirements, etc. Other than that, CloudFormation defaults should be fine as you step through the stack creation process.
Region | Alias | Launch URL |
---|---|---|
US East (N. Virginia) | us-east-1 | Launch Stack |
US East (Ohio) | us-east-2 | Launch Stack |
US West (N. California) | us-west-1 | Launch Stack |
US West (Oregon) | us-west-2 | Launch Stack |
Africa (Cape Town) | af-south-1 | Launch Stack |
Asia Pacific (Hong Kong) | ap-east-1 | Launch Stack |
Asia Pacific (Hyderabad) | ap-south-2 | Launch Stack |
Asia Pacific (Jakarta) | ap-southeast-3 | Launch Stack |
Asia Pacific (Melbourne) | ap-southeast-4 | Launch Stack |
Asia Pacific (Mumbai) | ap-south-1 | Launch Stack |
Asia Pacific (Osaka) | ap-northeast-3 | Launch Stack |
Asia Pacific (Seoul) | ap-northeast-2 | Launch Stack |
Asia Pacific (Singapore) | ap-southeast-1 | Launch Stack |
Asia Pacific (Sydney) | ap-southeast-2 | Launch Stack |
Asia Pacific (Tokyo) | ap-northeast-1 | Launch Stack |
Canada (Central) | ca-central-1 | Launch Stack |
Europe (Frankfurt) | eu-central-1 | Launch Stack |
Europe (Ireland) | eu-west-1 | Launch Stack |
Europe (London) | eu-west-2 | Launch Stack |
Europe (Milan) | eu-south-1 | Launch Stack |
Europe (Paris) | eu-west-3 | Launch Stack |
Europe (Spain) | eu-south-2 | Launch Stack |
Europe (Stockholm) | eu-north-1 | Launch Stack |
Europe (Zurich) | eu-central-2 | Launch Stack |
Israel (Tel Aviv) | il-central-1 | Launch Stack |
Middle East (Bahrain) | me-south-1 | Launch Stack |
Middle East (UAE) | me-central-1 | Launch Stack |
South America (São Paulo) | sa-east-1 | Launch Stack |
AWS GovCloud regions are not currently supported because the regions are missing provided.al2023
runtime support in Lambda.
The CloudFormation stack will have the status CREATE_COMPLETE
when the installation is finished. At this point, you can go to the "Outputs" tab in the stack and you'll see the following variables.
This is the Amazon Resource Name (Arn) of the Lambda function you'll invoke to sign JWTs. Examples of how to use it in the usage section below. This can be used as the value for function-name
(CLI) or function_name
(Ruby SDK) below.
Example: arn:aws:lambda:us-east-1:111111111111:function:JWTProd-JWTIssuerPSFunction-mUI2JR398C8c
This is the Amazon Resource Name (Arn) of the KMS key that is used when signing keys. This is left blank if using Parameter Store.
When signing tokens, this is the value that the kid
header claim will be set to in all JWTs. If using Parameter store, it's the UUID in the CloudFormation stack's ARN. If using KMS, it's the UUID in the KMS key ARN.
Example: ef814598-df45-4aa4-9f32-1b616ae6afda
This is the public key in PEM format encoded to Base 64. If you're using Hotsock, you can paste this value directly into the SigningKey1EncodedParameter
or SigningKey2EncodedParameter
to allow Hotsock to authorize signed keys from this stack.
It's completely harmless for this public key to be passed around. It's named appropriately!
Example: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFL2RmYXdYbkZxb0FWTG81NU04UW5yelBpazZOcgpYQnUybllLQkY5YTM2bGZtK0FPcG8xYzhxUzJKQkhYVVV1WE1YajAzdzh0Q1F0bGZidXFaaUljWGVnPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
If you decode this from Base 64 to a string, you'll see it's a PEM-formatted public key.
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/dfawXnFqoAVLo55M8QnrzPik6Nr
XBu2nYKBF9a36lfm+AOpo1c8qS2JBHXUUuXMXj03w8tCQtlfbuqZiIcXeg==
-----END PUBLIC KEY-----
This is the JWT signing algorithm. Always set to ES256
.
The release version of your installation.
Example: v1.0
First you need to grant your application the ability to invoke the JWT issuer Lambda function using IAM. At a minimum, an IAM policy tied to your application's AWS role or user must have Allow
set for the lambda:InvokeFunction
action on the Arn referenced in the JWTIssuerFunctionArn
output from your installation. If, for example, your application runs on AWS Fargate, you'd want to add this permissions policy to the task execution role for your ECS service. If your application runs on EC2, you probably need to add this permissions policy to the IAM role associated with your EC2 instance(s). You can also use IAM users with hard-coded credentials, but that's not recommended.
Here's a sample policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Action": ["lambda:InvokeFunction"],
"Effect": "Allow",
"Resource": [
"arn:aws:lambda:us-east-1:111111111111:function:JWTProd-JWTIssuerPSFunction-mUI2JR398C8c"
]
}
]
}
Using the AWS SDK in the language of your choice, call the Lambda invoke API to sign a token. Here's an example using the AWS CLI.
This generates a token with the aud
and channels
claims set explicitly and configures the exp
claim to expire the token 30 seconds after it is issued.
aws lambda invoke \
--function-name JWTIssuer-JWTIssuerPSFunction-MFlF1fyVpWkZ \
--payload '{"claims":{"aud":"hotsock","channels":{"chat":{"subscribe":true}}},"ttl":30}' \
--cli-binary-format raw-in-base64-out \
/dev/stdout
The response is JSON and contains the signed token in the token
field.
{
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImVmODE0NTk4LWRmNDUtNGFhNC05ZjMyLTFiNjE2YWU2YWZkYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJob3Rzb2NrIiwiY2hhbm5lbHMiOnsiY2hhdCI6eyJzdWJzY3JpYmUiOnRydWV9fSwiZXhwIjoxNzEzODM2OTUwfQ.Gz5iLG6O7YBQf8jAJafbaeCUxC08JnVEfnzbPOnn3S90hdiptlztp4Io3UmnhKjTqphf1G1ZYKQ29jbU7C6Xow"
}
Here's the same invocation, but using the Ruby SDK.
Aws::Lambda::Client.new.invoke(
function_name: "JWTIssuer-JWTIssuerPSFunction-MFlF1fyVpWkZ",
payload: JSON.dump({"claims":{"aud":"hotsock","channels":{"chat":{"subscribe":true}}},"ttl":30})
).payload.read
# => "{\"token\":\"eyJhbGciOiJFUzI1NiIsImtpZCI6ImVmODE0NTk4LWRmNDUtNGFhNC05ZjMyLTFiNjE2YWU2YWZkYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJob3Rzb2NrIiwiY2hhbm5lbHMiOnsiY2hhdCI6eyJzdWJzY3JpYmUiOnRydWV9fSwiZXhwIjoxNzEzODM2OTUwfQ.Gz5iLG6O7YBQf8jAJafbaeCUxC08JnVEfnzbPOnn3S90hdiptlztp4Io3UmnhKjTqphf1G1ZYKQ29jbU7C6Xow\"}"
Object
(required) - Provide all claims here as a JSON object.
Boolean
(optional) - If true, sets the iat
claim to the time that the token was issued. Overrides explicit iat
set in claims
. Defaults to false
.
Boolean
(optional) - If true, sets the jti
claim to a randomly generated UUID (v4). Overrides explicit jti
set in claims
. Defaults to false
.
Integer
(optional) - If supplied, sets the token expiration claim (exp
) to a timestamp this many seconds from when the token is issued. Overrides explicit exp
set in claims
. If not supplied, make sure you specify your own exp
claim in claims
to ensure the token expires.
You can assume that v1.x is stable. Updating an existing stack to the latest 1.x may add new functionality, but will not break existing APIs documented in this README, replace AWS resources, or change behavior. The underlying Go code may change at any time, as the code is not intended for use as a library imported into your code.
To update an existing stack, open CloudFormation in the AWS Console.
- Find your installation's stack (it's called JWTIssuer if you used the default name) and click the "Update" button.
- On the "Prepare template" screen, choose "Replace current template".
- For "Template source", use "Amazon S3 URL" and copy the URL for your region from the table below. Click "Next" through the screens that follow keeping all other defaults. Acknowledge any capabilities requirements on the final screen and click "Submit". Stack updates typically take no longer than 2 minutes.
Note: The above URLs will appear to not work if clicked on from a browser. They are only meant for use within CloudFormation. These templates are generated and written to S3 in all regions from GitHub Actions (.github/workflows/regional_templates.yml) when new releases are tagged.
Switching key custodians is not recommended. Technically, switching the KeyCustodianParameter
and updating the stack will do the right thing and change your preference. If you switch this way, your private/public keys will be deleted from KMS/Parameter Store during the update, the Lambda function used to sign keys will be replaced (and will have a different Arn in JWTIssuerFunctionArn
), and anything still attempting to sign with the previous keys will stop working immediately.
Instead, the recommendation is to launch a new stack that uses the desired service for key custody (Parameter Store or KMS). You can begin signing keys with the new installation immediately and delete the old stack once you've verified it is no longer needed.
To develop and test locally or to deploy a manual build, clone this repository and install the following.
- Install AWS SAM CLI
- Install Go (the latest release should work)
Run tests with make test
. Build all binaries for deployment on Lambda with make build
.
Use sam deploy --guided
to package local CloudFormation and deploy to a new stack using your AWS CLI credentials.