mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Enhance secret rotation tests and optimize JWT caching
Some checks failed
DB migration compat / Check if migrations changed (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
Some checks failed
DB migration compat / Check if migrations changed (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
- Updated test expectations in `secret-rotation.test.ts` to use `toContain` for content-type checks, improving clarity. - Introduced a bounded LRU cache mechanism in `getPrivateJwks` to manage cached entries effectively, ensuring optimal performance during secret rotations. These changes aim to improve test reliability and enhance the efficiency of JWT handling.
This commit is contained in:
parent
87ecaf6370
commit
ba00893be3
@ -29,7 +29,7 @@ const INTERNAL_JWKS_PATH = "/api/v1/projects/internal/.well-known/jwks.json";
|
||||
it("JWKS publishes 2 entries in steady state or 4 during rotation, all ES256 P-256, no duplicates, no private scalars", async ({ expect }) => {
|
||||
const response = await niceBackendFetch(INTERNAL_JWKS_PATH);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).includes("application/json");
|
||||
expect(response.headers.get("content-type")).toContain("application/json");
|
||||
expect(response.headers.get("cache-control")).toBe("public, max-age=3600");
|
||||
for (const key of response.body.keys) {
|
||||
expect(key).toEqual({
|
||||
|
||||
@ -118,6 +118,12 @@ async function getPrivateJwkFromDerivedSecret(derivedSecret: string, kid: string
|
||||
// Derivation is purely a function of those three inputs, so the cache is safe across calls
|
||||
// as long as the env vars are re-read on every call (which they are, below). Cached entries
|
||||
// stay valid even if env vars later change because the key includes the secret values.
|
||||
//
|
||||
// Bounded LRU: audience is typically a per-project identifier, so the keyspace grows with
|
||||
// the number of projects (and again on each secret rotation). Cap the cache and evict the
|
||||
// oldest entry on insert. Map iteration order is insertion order, so the first key returned
|
||||
// by keys() is the oldest. On a hit, we re-insert to bump recency.
|
||||
const PRIVATE_JWKS_CACHE_MAX = 1000;
|
||||
const privateJwksCache = new Map<string, Promise<PrivateJwk[]>>();
|
||||
|
||||
/**
|
||||
@ -131,7 +137,12 @@ export async function getPrivateJwks(options: {
|
||||
const oldSecret = getOldStackServerSecret();
|
||||
const cacheKey = JSON.stringify([primarySecret, oldSecret, options.audience]);
|
||||
const cached = privateJwksCache.get(cacheKey);
|
||||
if (cached) return await cached;
|
||||
if (cached) {
|
||||
// Bump recency: re-insert so this key becomes most-recently-used.
|
||||
privateJwksCache.delete(cacheKey);
|
||||
privateJwksCache.set(cacheKey, cached);
|
||||
return await cached;
|
||||
}
|
||||
|
||||
const derivePairForSecret = async (secret: string): Promise<PrivateJwk[]> => {
|
||||
const getHashOfJwkInfo = (type: string) => jose.base64url.encode(
|
||||
@ -165,6 +176,12 @@ export async function getPrivateJwks(options: {
|
||||
// Evict on rejection so a transient error doesn't poison the cache.
|
||||
computePromise.catch(() => privateJwksCache.delete(cacheKey));
|
||||
privateJwksCache.set(cacheKey, computePromise);
|
||||
// Evict oldest entries while over capacity (Map iterates in insertion order).
|
||||
while (privateJwksCache.size > PRIVATE_JWKS_CACHE_MAX) {
|
||||
const oldestKey = privateJwksCache.keys().next().value;
|
||||
if (oldestKey === undefined) break;
|
||||
privateJwksCache.delete(oldestKey);
|
||||
}
|
||||
return await computePromise;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user