npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cdk-jwks-secret

v0.1.0

Published

CDK construct for a JWKS stored in Secrets Manager and rotated by a Lambda

Readme

cdk-jwks-secret

CI npm License: MIT

An AWS CDK construct for a JWK Set stored in a Secrets Manager secret and rotated by a Lambda. The secret starts empty and is initialised with 2 keys by the rotation triggered when the stack is deployed. Every 28 days a new key is added, keeping at most 3, so that a key is published before it is used, e.g. for private_key_jwt client authentication with an OpenID Connect server.

| Reference documentation | | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Secrets Manager rotation | Rotate AWS Secrets Manager secrets | | private_key_jwt | OpenID Connect Core 1.0, section 9 | | JSON Web Key (JWK) | RFC 7517 | | JSON Web Algorithms (JWA) | RFC 7518 | | JWK thumbprint (kid) | RFC 7638 | | Guides | How rotation works, Using the keys |

Overview

Install the package; aws-cdk-lib and constructs are peer dependencies:

npm install cdk-jwks-secret

Then add a JwksSecret to a stack:

import { JwksSecret } from 'cdk-jwks-secret';

const jwksSecret = new JwksSecret(this, 'ClientJwks', {
  use: 'sig', // default; ES256 keys. Use 'enc' for ECDH-ES+A128KW keys.
});

jwksSecret.grantRead(serverTaskRole);

Your server reads the secret, serves its public keys from the client's jwks_uri and signs (or decrypts) with the private keys; see Using the keys.

Cost: the secret is retained by default when the stack is destroyed and costs $0.40 per month until it is deleted. See Removal and cost.

Construct Props

| Name | Type | Default | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | use? | JwkPublicKeyUse | The use of algorithm, or 'sig' | Whether the keys sign ('sig', e.g. for private_key_jwt) or encrypt ('enc', e.g. for encrypted ID tokens). Selects the default algorithm. If algorithm is also given, it must have this use. | | algorithm? | JwkAlgorithm | 'ES256' for sig, 'ECDH-ES+A128KW' for enc | The JWA algorithm of every key. For sig keys: RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512. For enc keys: RSA-OAEP-256 ECDH-ES ECDH-ES+A128KW ECDH-ES+A192KW ECDH-ES+A256KW. Determines the key type, the use and, for ES*, the curve. | | curve? | JwkEcCurve | Implied by algorithm; P-256 for ECDH-ES* | The EC curve: P-256, P-384 or P-521. Can only be chosen for ECDH-ES and ECDH-ES+A*KW. For ES* it must match the algorithm. | | rsaModulusLength? | number | 2048 | The RSA modulus length in bits, a multiple of 8 from 2048 to 4096. For RSA algorithms only. | | secretProps? | Pick<SecretProps, 'secretName' \| 'description' \| 'encryptionKey' \| 'removalPolicy'> | A generated name, no description, the AWS managed key aws/secretsmanager and RemovalPolicy.RETAIN | Overrides for the secret. Its value is always managed by the construct. It is retained by default because deleting it loses the keys the client registration relies on; see Removal and cost. | | rotationLambdaProps? | Pick<FunctionProps, 'memorySize' \| 'vpc' \| 'vpcSubnets' \| 'securityGroups'> | Not in a VPC. memorySize is 512 for RSA keys larger than 2048 bits, 128 otherwise. | Overrides for the rotation Lambda. Lambda allocates CPU in proportion to memory, which is what RSA key generation needs. In a VPC, the subnets need a route to Secrets Manager through a VPC endpoint or NAT. | | rotationLogGroupProps? | Pick<LogGroupProps, 'retention' \| 'removalPolicy'> | RetentionDays.ONE_YEAR and the secret's removal policy | Overrides for the rotation Lambda's log group. | | rotationScheduleProps? | Pick<RotationScheduleOptions, 'automaticallyAfter'> | automaticallyAfter: Duration.days(28) | Overrides for the rotation schedule. automaticallyAfter must be from 4 hours to 1000 days. It is also how long a new key is published before it is used, so it should be longer than the OpenID Connect server's JWKS cache lifetime. The schedule always rotates immediately when it is created or updated, which initialises the secret. |

The key options (use, algorithm, curve, rsaModulusLength) cannot be changed on an existing secret: the next rotation fails and leaves the secret unchanged. See Changing the key options.

Construct Properties

| Name | Type | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | secret | secretsmanager.Secret | The secret holding the JWKS. | | rotationSchedule | secretsmanager.RotationSchedule | The secret's rotation schedule. | | rotationLambda | lambda.Function | The rotation Lambda. | | rotationLogGroup | logs.LogGroup | The rotation Lambda's log group. | | rotationLambdaRole | iam.Role | The rotation Lambda's execution role. | | jwkOptions | ResolvedJwkOptions | The resolved key options: algorithm, use, keyType, and curve or rsaModulusLength. | | grantRead(grantee) | method, returns iam.Grant | Grants grantee read access to the secret (and decrypt on encryptionKey), e.g. the server that serves the JWKS and signs with the keys. |

AWS resources

JwksSecret creates:

| AWS resource | CloudFormation type | Purpose | Cost | | ------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Secret | AWS::SecretsManager::Secret | Holds the JWKS. Retained by default. | $0.40 per month and $0.05 per 10,000 API calls | | Secret resource policy | AWS::SecretsManager::ResourcePolicy | Added by the CDK: denies DeleteSecret to the account while the stack exists | Free | | Rotation schedule | AWS::SecretsManager::RotationSchedule | Rotates every 28 days, and immediately on creation | Free | | Rotation Lambda | AWS::Lambda::Function | Generates and rotates the keys | Per invocation; a few seconds per rotation | | Lambda permission | AWS::Lambda::Permission | Lets Secrets Manager invoke the rotation Lambda | Free | | Lambda role | AWS::IAM::Role, AWS::IAM::Policy | Lets the rotation Lambda read and update the secret, and write its own logs | Free | | Log group | AWS::Logs::LogGroup | Rotation Lambda logs, kept 1 year. Retained by default. | Storage; a few KB per rotation | | Security group (with rotationLambdaProps.vpc only) | AWS::EC2::SecurityGroup | Network access for the rotation Lambda in the VPC | Free | | Key policy statements (with secretProps.encryptionKey only) | modifies your AWS::KMS::Key | Let the rotation Lambda use the key through Secrets Manager | None beyond the key's own cost |

Default settings

Out of the box implementation of the Construct without any override will set the following defaults:

AWS Secrets Manager secret

  • Created with the secret string {"keys":[]}; no key material passes through CloudFormation
  • Encrypted with the AWS managed key aws/secretsmanager
  • Retained when removed from the stack (DeletionPolicy: Retain)
  • A resource policy, added by the CDK, denies secretsmanager:DeleteSecret to every principal in the account while the stack exists

Secrets Manager rotation schedule

  • Rotates every 28 days
  • Rotates immediately when the schedule is created, which initialises the secret with 2 keys, and whenever the schedule is updated (see Timing rules)
  • Each rotation adds a new private key, keeps at most 3 keys and, for sig keys, removes the private part of the oldest key

AWS Lambda function (rotation)

  • Node.js 24 on arm64, 1 minute timeout
  • 128 MB memory, or 512 MB for RSA keys larger than 2048 bits, which take far more CPU to generate
  • Code pre-bundled in the package; nothing is bundled when the consumer synthesizes
  • Generates ES256 keys on P-256 (ECDH-ES+A128KW on P-256 with use: 'enc'), each with kid (its RFC 7638 thumbprint), use and alg
  • Logs only key ids, never key material
  • Not in a VPC

AWS Lambda permission

  • Allows secretsmanager.amazonaws.com to invoke the rotation Lambda

AWS IAM role and policy

  • Execution role without AWS managed policies
  • Allows logs:CreateLogStream and logs:PutLogEvents on the rotation Lambda's own log group only
  • Allows secretsmanager:DescribeSecret, GetSecretValue, PutSecretValue and UpdateSecretVersionStage on the secret
  • Allows secretsmanager:GetRandomPassword (added by the CDK's rotation schedule; unused)

Amazon CloudWatch Logs log group

  • Holds the rotation Lambda's logs for 1 year
  • Retained when removed from the stack

Optional resources

  • With rotationLambdaProps.vpc: a security group for the rotation Lambda (unless securityGroups is given), allowing all outbound traffic, and the AWSLambdaVPCAccessExecutionRole managed policy on its role
  • With secretProps.encryptionKey: key policy statements allowing the rotation Lambda to encrypt and decrypt with the key through Secrets Manager

cdk-nag

The construct passes the cdk-nag AwsSolutionsChecks rules. It acknowledges two findings on the rotation Lambda's role with CDK's Validations.of(...).acknowledge(...), so they appear as acknowledged, with these reasons, in your validation report:

| Finding | Reason | | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | AwsSolutions-IAM5[Resource::*] | secretsmanager:GetRandomPassword, added by the CDK rotation schedule, does not support resource-level permissions and reads no data. | | AwsSolutions-IAM4[Policy::…AWSLambdaVPCAccessExecutionRole] (with rotationLambdaProps.vpc only) | Lambda needs these network interface permissions to run in a VPC; they do not support resource-level permissions. |

cdk-nag identifies these findings only by rule and policy, not by statement, so the acknowledgements cover the whole rotationLambdaRole. Don't add your own permissions to that role (e.g. with rotationLambda.addToRolePolicy): a wildcard permission you add would be acknowledged with the reasons above instead of being reported.

Architecture

flowchart LR
  subgraph construct["JwksSecret"]
    schedule["Rotation schedule<br/>every 28 days"] -- invokes --> rotation["Rotation Lambda"]
    rotation -- "reads and writes versions" --> secret[("Secret<br/>JWKS")]
    rotation -- logs --> logGroup["Log group"]
  end
  server["Your server"] -- GetSecretValue --> secret
  server -- "serves public keys" --> jwksUri["jwks_uri"]
  op["OpenID Connect server"] -- "fetches and caches" --> jwksUri
  server -- "private_key_jwt" --> op

Removal and cost

By default the secret and the rotation Lambda's log group are retained when they are removed from the stack or the stack is destroyed, because deleting the secret loses the keys the client registration relies on.

A retained secret keeps costing $0.40 per month until it is deleted, although it no longer rotates. The resource policy that prevents deletion is removed with the stack, so the secret can then be deleted:

aws secretsmanager delete-secret --secret-id <arn> --recovery-window-in-days 7

(or --force-delete-without-recovery). A secret scheduled for deletion is not charged. For development and test stacks, pass secretProps: { removalPolicy: RemovalPolicy.DESTROY } instead; the log group follows it.

Guides

  • How rotation works: the key lifecycle for sig and enc keys, the rotation steps and caveats
  • Using the keys: what your server must do to serve the JWKS endpoint and sign or decrypt, and the cdk-jwks-secret/jwks helpers

Example

The repository's bin/ folder contains a deployable example app, JwksSecretExampleApp, for an OpenID Connect client that both signs and decrypts:

  • a sig JwksSecret (ES256) and an enc JwksSecret (ECDH-ES+A128KW), both destroyed with the stack
  • a JWKS endpoint Lambda behind a public Function URL that serves the public keys of both secrets in one JWKS, as a client's jwks_uri would

So that a rotation shows up straight away, the example doesn't cache the secrets by default; a real endpoint would cache them for a few minutes.

On top of the resources of the two JwksSecrets, it creates:

| AWS resource | CloudFormation type | Purpose | Cost | | ------------------------ | ------------------------------------ | ------------------------------------------------------------ | ----------------------------------------------------- | | Endpoint Lambda | AWS::Lambda::Function | Serves the public keys of both secrets | Per request | | Function URL | AWS::Lambda::Url | Public HTTPS address of the endpoint (output JwksUrl) | Free | | Function URL permissions | AWS::Lambda::Permission (2) | Allow anyone to invoke the endpoint through the Function URL | Free | | Endpoint role | AWS::IAM::Role, AWS::IAM::Policy | Lets the endpoint read both secrets | Free | | Endpoint log group | AWS::Logs::LogGroup | The endpoint's logs, kept 1 week | Storage |

The example's secrets and all its log groups (kept 1 week) are destroyed with the stack, so cdk destroy leaves nothing behind.

To try it, clone the repository and deploy it to the AWS account and region of your current credentials:

npm ci
npm run cdk:deploy                    # outputs JwksUrl, SigSecretArn and EncSecretArn
curl <JwksUrl>                        # 2 sig + 2 enc keys, shortly after the deployment
aws secretsmanager rotate-secret --secret-id <SigSecretArn>
curl <JwksUrl>                        # 3 sig + 2 enc keys, once the rotation completes (seconds)
aws secretsmanager rotate-secret --secret-id <EncSecretArn>
curl <JwksUrl>                        # 3 sig + 2 enc keys: the oldest of 3 enc keys is not published
npm run cdk:destroy

Choose the algorithms, rotation interval and endpoint cache duration with CDK context, e.g. npm run cdk:deploy -- -c sigAlgorithm=PS256 -c encAlgorithm=RSA-OAEP-256 -c rotationIntervalDays=1 -c jwksCacheSeconds=300.

Contributing

Bug reports and pull requests are welcome. See CONTRIBUTING.md for how to build and test the project, and SECURITY.md for reporting vulnerabilities.

License

MIT