Repository navigation
feat(auth): cache API key lookups with hit rate metrics and revocation invalidation (#495) - #573
Conversation
tsc --noEmit clean vitest 102 files, 1157 passing #563 extend the env drift guard to examples and scripts #568 let callers read their own usage #571 zero-downtime admin key rotation #578 pin the connect-time address to defeat DNS rebinding #563 earned itself immediately. Extending the guard beyond src/ caught two variables read by the live-spec validator that were never documented: LIVE_API_URL (scripts/validate-live-spec.ts:15) LENS_URL (scripts/validate-live-spec.ts:15) That script came in with #318, which I merged earlier today, so this is a gap I let through two batches ago. Both are now in .env.example with a note on the precedence between them. Its own test case was flagged by the dropped-test guard and turned out to be split rather than deleted -- "fails when a new un-documented process.env read is encountered" became one case for src/ and one for examples/. Third time today that guard surfaced a rename; it has yet to find a real deletion, which is the outcome you want from it. #578 closes the SSRF rebinding hole: resolving a host, checking it, then letting the HTTP client resolve it again leaves a window where the second answer differs from the one that passed the check. Pinning the connect-time address removes the window rather than narrowing it. Two from this group were NOT merged, both failing on their own branch: #572 (capability scopes per route) 1 type error alone: src/api/auth.ts(165): Property 'scope' does not exist on type '{ public?: boolean | undefined; }' #577 (prisma migrate with a baseline) 1 failing test alone -- its own: "has an init/baseline migration that reproduces schema.prisma exactly" #573 #574 #575 #576 conflict. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
4ac86a7 to
f1f2111
Compare
|
Thanks for this. It has been overtaken: issue #495 was assigned to another contributor, and their PR (#594) is merged as You and they arrived at the same architecture independently: in-process Map keyed by the key hash, both positive and negative lookups cached, a short TTL, explicit invalidation on revoke, and a Three differences decided it, and all three are worth knowing because they generalise to any cache in front of an authorization check. 1. Caching the result rather than the record means expiry stops being re-checked. const cached = apiKeyLookupCache.get(hash)
if (cached && now - cached.cachedAt < ttlMs) {
return cached.result
}
function evaluate(value: CachedKey, now: number) {
if (!value.found) return { context: null, failureReason: 'unknown' }
if (value.revoked) return { context: null, failureReason: 'revoked' }
if (value.expiresAt && value.expiresAt.getTime() <= now) {
return { context: null, failureReason: 'expired' }
}
return { context: value.context }
}That is what makes "the TTL can never outlive the key" true rather than approximately true, and it matters most for key rotation, where the whole point is a key that expires at a known moment. 2. The cache is unbounded. Negative lookups are cached — correctly, otherwise an invalid key is a free way to hammer Postgres — but 3. The TTL is disabled under if (process.env.NODE_ENV === 'test' && !process.env.API_KEY_CACHE_TTL_SECONDS) {
return 0
}I would push back on this one hardest. Production code branching on the test environment means the default The two places it would have bitten are exactly the two tests that went red when #594 merged and had to be fixed: The way to do it is to let tests control the state rather than the config: export a One smaller thing: There are unclaimed issues open if you want another — comment on one to get it assigned before you start, so you are not racing an assignee again. Three of your PRs have now collided with assigned issues (#415, #505, #495), and that is wasted effort I would rather you spent on something that lands. |
|
@Miracle656 Closing this out as Issue #495 has already been resolved and merged via PR #594. Glad to see the caching and invalidation improvements landed on main! |
Description
Fixes #495.
Summary of Changes
Every authenticated request previously paid an obligatory database round-trip to Postgres in the
onRequestauth hook before any handler or Redis price cache could execute.This PR implements fast in-memory key lookup caching according to all issue criteria:
apiKeyLookupCachewith configurableAPI_KEY_CACHE_TTL_SECONDS(default: 10 seconds).DELETE /admin/keys/:idinvokesinvalidateApiKeyCache()upon key revocation, making local admin revocation instantaneous (0ms lag).api_key_cache_requests_totalcounter labeled byresult: 'hit' | 'miss'to track cache hit rate and health in Prometheus.src/__tests__/authCache.test.tsverifying repeated calls avoid Postgres, negative lookups are shielded, metrics increment accurately, and key revocation invalidates the cache immediately.Testing
npx vitest run src/__tests__/authCache.test.ts(3/3 passed)npx vitest run src/__tests__/auth.test.ts(22/22 passed)npx vitest run tests/openapi.test.ts(8/8 passed)