npm package: @geostrategists/cdk-jwks-rotation
Description: CDK construct for generating and rotating JWT signing keys and JSON Web Key Sets (JWKS)
Author: Bernhard Frauendienst
Latest version: 3.0.0

# CDK JWKS Rotation Construct

This AWS CDK construct creates and manages cryptographic key pairs for signing JSON Web Tokens (JWTs).
It stores the private key securely in AWS Secrets Manager and publishes the public key as a JWKS (JSON Web Key Set) file
in an S3 bucket.
The construct provides seamless, automated key rotation.

There is a [blog post by Jan Brennenstuhl
at Zalando](https://engineering.zalando.com/posts/2025/01/automated-json-web-key-rotation.html)
which nicely explains the general concept.

The construct uses AWS Secrets Manager rotation to manage the lifecycle of the keys. 
Whenever a new key is rotated in, the next key is already generated, and its public key is added to the JWKS file.
This allows consumers to have refetched the JWKS in time, and be prepared when the next key is activated.
The next key is stored in the same secret in a separate version labeled "NEXT".
Whenever a key is retired and the maximum token lifetime has passed, the corresponding public key is removed from 
the JWKS file (after a configurable grace period).

## Installation

```bash
npm install @geostrategists/cdk-jwks-rotation
# or
yarn add @geostrategists/cdk-jwks-rotation
```

## Types

### SecretValue

The `SecretValue` interface represents the structure of cryptographic key data stored in AWS Secrets Manager:

```ts
interface SecretValue {
  /** The private key in PEM format used for signing JWTs */
  privateKeyPem: string;
  /** The public key in JWK format for inclusion in JWKS */
  publicKeyJwk: JWK;
  /** Unique key identifier used in JWT headers and JWKS */
  kid: string;
  /** Algorithm used for signing (e.g., RS256, ES256) */
  alg: string;
  /** ISO timestamp when the key was created */
  createdAt: string;
  /** ISO timestamp when the key was activated for signing (optional) */
  activatedAt?: string;
}
```

## Usage

### Mandatory Props

- `secretName`: Name for the AWS Secrets Manager secret to store the private key.
- `keySpec`: Specification for the key pair (algorithm, curve, modulus length, etc.). See [jose documentation](https://github.com/panva/jose/blob/HEAD/docs/key/generate_key_pair/functions/generateKeyPair.md)
- `maxTokenValidityDuration`: Maximum duration for which a token signed by a key is considered valid. This will influence how long a retired key is kept in the JWKS file.

### Optional Props

- `bucket`: Existing S3 bucket to store the JWKS file. By default, a new bucket will be created.
- `bucketPath`: Path/name for the JWKS file in the bucket. Default: `".well-known/jwks.json"`.
- `bucketName`: Name for a new S3 bucket (if `bucket` is not provided). By default, a name will be generated by CKD (recommended).
- `rotationInterval`: How often to rotate keys. Default: `Duration.days(60)`.
- `minActivationGracePeriod`: Grace period before a new key is activated. Default: `Duration.days(7)`.
- `cleanupCheckInterval`: How often to check for old keys to clean up. (Doing this too often can influence costs). Default: `Duration.hours(24)`.
- `minKeyCleanupGracePeriod`: Grace period before deleting old keys after max token validity duration has passed. Default: `Duration.hours(6)`.

```ts
import { JwksRotation } from 'cdk-jwks-rotation';

new JwksRotation(this, 'JwksRotation', {
  secretName: 'my-jwks-secret',
  keySpec: { algorithm: 'ES256' },
  maxTokenValidityDuration: Duration.seconds(3600),
  // Optional, these are the default values:
  bucket: undefined, // or: myBucket,
  bucketPath: ".well-known/jwks.json",
  rotationInterval: Duration.days(60),
  minActivationGracePeriod: Duration.days(7),
  cleanupCheckInterval: Duration.hours(24),
  minKeyCleanupGracePeriod: Duration.hours(6),
});
```

## IAM and permissions

The construct grants the rotation Lambda:
- Secrets Manager: DescribeSecret, GetSecretValue, PutSecretValue, UpdateSecretVersionStage (scoped to the created secret)
- S3 bucket read/write for the JWKS object

## Troubleshooting

- Rotation aborted with "Next key is too new": Allow more time (per `minActivationGracePeriod`) between creating NEXT and a rotation cycle, or lower the grace period if appropriate.
- "No keys found in JWKS document": Ensure initial deployment updated JWKS; re-run rotation or check Lambda permissions to S3.
- JWT verification failed during `testSecret`: Verify the JWKS URL and that the new public key was written; check S3 object and cache.

## Potential issues / room for improvement (as identified by AI)

- Bucket exposure flexibility: Consider a prop to toggle bucket `publicReadAccess` and optionally attach a CloudFront distribution for best practice public serving.
- Custom KMS support: Allow passing a customer-managed KMS key for the secret.
- JWKS cache-control configuration: Make `CacheControl` configurable to align with consumer caching SLAs.
- Health endpoint: Optionally emit a small metric or a health-check log/CloudWatch metric on successful rotation to aid monitoring/alerts.
- Key ID strategy: Current `kid` uses `nanoid()`. Some teams prefer deterministic KIDs (e.g., hash of public key). Consider making strategy pluggable.
- Broader algorithm validation: Enforce that ES algorithms are paired with a required `crv`, and validate modulus length ranges for RSA in the construct props.

## Development

Install and scripts:
```bash
yarn install
yarn lint
yarn typecheck
yarn build
yarn test:run
```