feat(api-gateway): accept a list of API secrets for verification#10985
Conversation
|
Claude finished @bsod90's task in 2m 10s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM with minor suggestions. 0 high, 1 medium, 3 low. Implementation is clean, well-scoped, and well-tested. Iterating candidate secrets for HS-style verification is the right primitive for zero-downtime rotation, and the carve-outs ( Details (todos, findings, suggestions)Todos
FindingsMedium
Low
Other observations
|
| for (const candidate of candidateSecrets) { | ||
| try { | ||
| req.securityContext = await checkAuthFn(auth, candidate as string); | ||
| req.signedWithPlaygroundAuthSecret = Boolean(internalOptions?.isPlaygroundCheckAuth); | ||
| verified = true; | ||
| break; | ||
| } catch (e: any) { | ||
| lastError = e; | ||
| if (isJWK) { | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
When verification fails for all candidates, only lastError is reported — but for a truly invalid token, every candidate produces a similar JsonWebTokenError: invalid signature and the last one is fine. The concerning case is when the token is structurally valid but failed for a non-signature reason (TokenExpiredError, audience/issuer mismatch). Today such an error from secret #1 is overwritten by an "invalid signature" from secret #N, masking the real cause and degrading the 403 message. Consider preferring a non-JsonWebTokenError (e.g. TokenExpiredError, NotBeforeError) over a generic signature failure when picking which error to bubble up.
73b32aa to
4c716e9
Compare
|
Claude finished @bsod90's task in 37s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM with minor suggestions. 0 high, 1 medium, 3 low. Re-affirmed 4 prior unresolved threads — the diff is unchanged since the previous review (still a single commit Details (todos, findings)Todos
Findings (still applicable)Medium
Low
Other observations
· branch: |
4c716e9 to
cd792cf
Compare
|
Claude finished @bsod90's task in 1m 33s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM. 0 high, 0 medium, 1 low. New commit DetailsTodos
What changed since the prior review
Residual (Low)
Other observations
|
cd792cf to
16d5427
Compare
|
Claude finished @bsod90's task in 56s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM. 0 high, 0 medium, 1 low (residual). No diff changes since the prior review at 16:56:05Z — the branch is still a single commit ( DetailsTodos
State
Residual (Low)
Other observations
· branch: |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10985 +/- ##
===========================================
- Coverage 78.75% 58.54% -20.22%
===========================================
Files 470 216 -254
Lines 93902 17175 -76727
Branches 3466 3501 +35
===========================================
- Hits 73953 10055 -63898
+ Misses 19446 6610 -12836
- Partials 503 510 +7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Adds `CUBEJS_API_SECRETS` (comma-separated) as a sibling of `CUBEJS_API_SECRET`. When set, `CUBEJS_API_SECRETS` takes precedence: the gateway tries each secret in the list when verifying a JWT and accepts the token if any of them matches the signature. Signing behavior is unchanged — callers still sign with whatever single secret they hold. This enables zero-downtime rotation of the API secret. During a rotation window operators can publish the outgoing, current, and incoming secrets together in `CUBEJS_API_SECRETS`; in-flight tokens signed by the outgoing secret keep verifying until they expire or are reissued. - New optional `apiSecrets: string[]` on `ApiGatewayOptions` and `CreateOptions` (server-core). - `createDefaultCheckAuth` iterates `apiSecrets` when present; otherwise falls back to the singular `apiSecret`. An explicit `options.key` (used by the playground/system auth path) still wins, so playground behavior is unchanged. - JWK auth is unaffected — when `jwkUrl` is configured the secret is fetched dynamically from JWKS, so iterating a static list would be meaningless. - `Cube.apiSecrets()` static accessor mirrors `Cube.apiSecret()`. Tests cover: accept any listed secret, reject unlisted, list takes precedence over singular, empty list falls back, playground path isolated.
16d5427 to
1c0118c
Compare
|
Claude finished @bsod90's task in 1m 11s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM. 0 high, 0 medium, 1 low (residual). Branch was squashed/force-pushed to a single commit DetailsTodos
State
Residual (Low)
Other observations
· branch: |
…Auth Instead of looping over candidate secrets in the gateway's returned auth middleware, the default checkAuth implementation now tries each configured secret internally. The middleware reverts to a single checkAuthFn(auth) call with one try/catch, matching the original structure. This keeps the secret-rotation logic where it belongs (the default token verifier) and avoids invoking the verifier wrapper from an outer loop. JWK auth resolves its key dynamically by kid, so it remains a single-secret path. Expiry/nbf failures still short-circuit so the real cause isn't shadowed by a later "invalid signature". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @bsod90's task in 1m 27s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM. 0 high, 0 medium, 1 low (residual, unchanged). New commit DetailsWhat changed since the prior review (
|
| externalDriverFactory, | ||
| externalDialectFactory, | ||
| apiSecret: process.env.CUBEJS_API_SECRET, | ||
| apiSecrets: parseApiSecretsEnv(process.env.CUBEJS_API_SECRETS), |
…secrets
Addresses code-review feedback:
1. Route env access through `@cubejs-backend/shared` env.ts instead of
reading process.env directly. Adds `apiSecret` and `apiSecrets`
accessors (the latter trims, drops empties and deduplicates), and
switches OptsHandler and CubejsServer.apiSecret()/apiSecrets() to
getEnv(). Removes the now-redundant apiSecretsEnv helper + its test;
parsing/dedup coverage moves to env.test.ts.
2. Adds an api-gateway test for CUBEJS_API_SECRETS coexisting with
playgroundAuthSecret: playground token and any listed secret are
accepted, while the shadowed singular apiSecret and an unknown secret
are rejected.
3. Verified consistency with cube-runtime: CloudApiGateway does not
override createDefaultCheckAuth/createCheckAuthFn (it only overrides
createCheckAuthSystemFn for a playground-secret list, calling
createDefaultCheckAuth({ key }) per secret). The options.key-first
precedence keeps that path single-key, so no override conflict.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @bsod90's task in 55s —— View job Review — feat(api-gateway): accept a list of API secretsVerdict: LGTM. 0 high, 0 medium, 1 low (residual, unchanged). Already approved by @paveltiunov. New commit DetailsWhat changed since the prior review (
|
Adds
CUBEJS_API_SECRETS(comma-separated) as a sibling ofCUBEJS_API_SECRET. When set,CUBEJS_API_SECRETStakes precedence: the gateway tries each secret in the list when verifying a JWT and accepts the token if any of them matches the signature. Signing behavior is unchanged — callers still sign with whatever single secret they hold.This enables zero-downtime rotation of the API secret. During a rotation window operators can publish the outgoing, current, and incoming secrets together in
CUBEJS_API_SECRETS; in-flight tokens signed by the outgoing secret keep verifying until they expire or are reissued.apiSecrets: string[]onApiGatewayOptionsandCreateOptions(server-core).createDefaultCheckAuthiteratesapiSecretswhen present; otherwise falls back to the singularapiSecret. An explicitoptions.key(used by the playground/system auth path) still wins, so playground behavior is unchanged.jwkUrlis configured the secret is fetched dynamically from JWKS, so iterating a static list would be meaningless.Cube.apiSecrets()static accessor mirrorsCube.apiSecret().Tests cover: accept any listed secret, reject unlisted, list takes precedence over singular, empty list falls back, playground path isolated.
Check List