Repository navigation
Conversation
…S_SECRET Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut new options Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…secret Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Vercel, or locally after `vercel env pull`, verify override tokens which the Vercel Toolbar signs with Vercel KMS, alongside FLAGS_SECRET tokens. Share the host project lookup and the JWKS cache with the platform encryption of the flag values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Luis Meyer <41523937+luismeyer@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lues Rename vercel-overrides to flag-overrides-platform, and use the same shape as flag-values-platform: an exported issuer, JWKS URL and frozen policy, a publicKeys cache, and the FromPlatform/ForPlatform names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the verifier of Vercel-signed tokens into vercel-platform, and the flags discovery issuer and policy into flags-discovery-platform, next to flag-overrides-platform and flag-values-platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apps which read the vercel-flag-overrides cookie themselves call decryptOverrides, so it verifies overrides signed by Vercel KMS too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rcel-platform Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
chriswdmr
left a comment
There was a problem hiding this comment.
LGTM, nothing blocks this PR. Things to consider:
- Keep
verifyAccessworking where there is noprocessglobal (the cause isisVercelSignedTokenfrom #587) - Update the
verifyAccessdocs for the Vercel-signed token - Warn one time when there is no secret outside Vercel
The rest are three questions about the tokens and three test nits.
| * You can use verifyAccess to keep this endpoint private, to avoid public access of your feature flag definitions through that endpoint. | ||
| * | ||
| * It accepts a proof encrypted with the FLAGS_SECRET, and, on Vercel (or locally after `vercel env pull`), | ||
| * a short-lived token which Vercel signs for this project and environment. The latter works without a FLAGS_SECRET. |
There was a problem hiding this comment.
question: Apps on an older flags version
An app on an older flags version that has FLAGS_SECRET treats a signed token as an encrypted proof, and responds with 401. Do such apps continue to get the encrypted proof?
There was a problem hiding this comment.
Yes. Both senders look at the secret, not at the SDK version:
- The service that discovers the flags of a deployment sends the encrypted proof when the deployment has a
FLAGS_SECRET, and requests a signed token only when it has none - The toolbar gets no signed token for a project that has a
FLAGS_SECRET
So an app with a secret continues to get the encrypted proof on each flags version.
| */ | ||
| export const FLAGS_DISCOVERY_POLICY = Object.freeze({ | ||
| algorithm: 'ES256', | ||
| audience: 'vercel:flags-discovery', |
There was a problem hiding this comment.
question: Is this audience only for discovery tokens?
The PR description says that more than one signer uses this issuer. The verifier keeps tokens apart only by aud, pur and ver. Can you confirm that only discovery tokens get aud: 'vercel:flags-discovery'?
There was a problem hiding this comment.
Yes. Two signers use this issuer: the service that discovers the flags of a deployment, and the toolbar. Both sign only discovery tokens, with aud: 'vercel:flags-discovery' and pur: 'flags-discovery'. Signed overrides use a different issuer.
…s-discovery Co-Authored-By: Claude Code <noreply@anthropic.com>
- Warn when verifyAccess has no secret on a host which can not verify signed tokens - SvelteKit: load $env in createFlagsDiscoveryEndpoint, so signed tokens work in vite dev - Tests use the real issuer; remove the issuer mock - Move the Next.js endpoint tests to src/next/index.test.ts - Tests for maxTokenLength and custom environments Co-Authored-By: Claude Code <noreply@anthropic.com>
What / why
Lets v4 apps serve
/.well-known/vercel/flagsto Vercel withoutFLAGS_SECRET.verifyAccessnow also accepts a short-lived JWS that Vercel KMS signs. The existing FLAGS_SECRET JWE proof path still works the same way.verifyAccess(authHeader, secret = process.env.FLAGS_SECRET): the signature is unchanged. It stripsBeareras before. IfisVercelSignedTokensays the token is a JWS the app can verify, the token goes to the KMS discovery verifier, whether or not FLAGS_SECRET is set. Any other token goes toverifyAccessProofas before.verifyAccessnow returnsfalseinstead of throwing.vercel-platform.ts: the shared verifierverifyVercelSignedToken(token, issuer, policy)(withisVercelSignedToken, the per-issuer JWKS caches, header checks andisInScope).flag-overrides-platform.ts([flags] accept Vercel-signed override tokens #587) calls it withFLAG_OVERRIDES_POLICY; the newflags-discovery-platform.tsholdsVERCEL_FLAGS_DISCOVERY_ISSUERandFLAGS_DISCOVERY_POLICY, whichverifyAccesspasses in. Overrides behave the same as before: they still use theflags-explorer-issuer.flags-keys-issuer(VERCEL_FLAGS_DISCOVERY_ISSUER). Both the toolbar and the internal flags-keys endpoint sign with it. There is one cached JWKS loader per issuer (${issuer}/jwks.json), so one issuer's keys never verify the other's tokens, and theissclaim must match exactly.createHandle: removed theif (!secret) return 401guard in front ofverifyAccess, so a no-secret app can serve discovery. The Next and SvelteKitcreateFlagsDiscoveryEndpointneeded no change. No other caller requires FLAGS_SECRET on the discovery path.Token contract
flags-keys-issuer(VERCEL_FLAGS_DISCOVERY_ISSUER=https://kms.vercel.com/d0efbb30-566d-48b6-b173-0062e54ec49c). Overrides stay onflags-explorer-issuerhttps://kms.vercel.com/64af96a2-ba60-42ef-91af-459fcef6f864. Thekidheader is required. Tokens withjku,jwk,x5uorcritare rejected.iss,iatandexpare required, withaud: 'vercel:flags-discovery',pur: 'flags-discovery'andver: 1.scope: { teamId, projectIds, environments }uses the sameisInScopeas overrides. The app's project must be inprojectIds. The team must match when it is known (see teamId below). The app's environment must be inenvironments. Unknown keys are rejected.exp - iat <= 300andmaxTokenAge300), with 5s clock tolerance and a max token length of 16 KiB.standard,src/overrides/vercel.ts), checked by reading the code: it rejects discovery tokens. jose checksaudience: 'vercel:flag-overrides'exactly,pur === 'flag-overrides'is required, and anoverridesobject is required.teamId cannot be enforced on deployments
isInScopeonly checksscope.teamIdwhen the team is known. The only source isVERCEL_OIDC_TOKENinprocess.env, which exists locally aftervercel env pulland during builds. On a deployment at runtime, the OIDC token is only a per-requestx-vercel-oidc-tokenheader, not an env var. None of the Vercel system env vars (VERCEL_PROJECT_ID,VERCEL_ENV,VERCEL_TARGET_ENV,VERCEL_DEPLOYMENT_ID,VERCEL_URL,VERCEL_GIT_*, …) contains the team id. So on deployments the team is not checked. The project id is globally unique and comes from the host, never from the token. The project id and the environment still bind the token, and both are enforced. This is now documented in code atisInScope, and nothing was weakened: the team is still enforced whenever it is known.Tests
packages/flags:pnpm type-check: okpnpm check(biome): no issuespnpm test: 15 files, 228 tests passedpnpm build: okpnpm attw: no problems foundNew
verify-access.test.ts(the JWKS fetch is mocked with a locally generated ES256 key):jwk/jkuheader, overrides token, wrong project/env/team, extra scope key, unknown project.verifyAccess, and an overrides token from the flags-keys issuer is rejected byverifyFlagOverridesFromPlatform, even when both JWKS serve the same key. Each issuer's keys are fetched from its own JWKS URL.VERCEL_FLAGS_DISCOVERY_ISSUERwith a test UUID issuer.falsewithout throwing.sveltekit/without-secret.test.ts:createHandleandcreateFlagsDiscoveryEndpointreturn 200 for a KMS token and 401 without one, all without a secret.Open questions
Should the team be read from the per-request
x-vercel-oidc-tokenheader (@vercel/request-context)? It would only add a restriction, but it isn't always present (OIDC can be disabled, and there are no headers at module scope). Itsowner_idwould also have to match what the signer puts inscope.teamId, for example on Hobby accounts. This PR doesn't do that.Behaviour change:
verifyAccesswithout a secret used to throw and now returnsfalse. Custom routes that relied on the throw now get a 401 instead of an error.Outside Vercel, without
VERCEL/VERCEL_OIDC_TOKEN, a JWS still goes to the JWE path, as in [flags] accept Vercel-signed override tokens #587. That means localnext devwithoutvercel env pullcan't use KMS tokens.SvelteKit
createFlagsDiscoveryEndpointstill falls back toprocess.env.FLAGS_SECRETrather than$env/static/private. That behaviour was already there and is unchanged.Snapshot:
flags@4.5.0-a5fdf7a-20261109093837, with the real issuer.🤖 Generated with Claude Code