fix(init): write the signing key id a managed instance publishes - #191
Conversation
Managed scaffolds hardcoded JWKS_KID=dev-main while instances pin their kid per tier (trialkey1, paidkey1), so the value was never the instance's own. Nothing verifies against it: adapters mint service tokens with HS256 and a shared secret, where kid is a decorative header the API never reads, and user access tokens go through createRemoteJWKSet, which selects the key by the token header's kid. But the adapters warn on boot while jwksKid is the dev default, so every managed scaffold shipped an app reporting itself misconfigured. init now reads the kid from /.well-known/jwks.json. An unreachable instance keeps the old default with a note explaining that only the warning is affected, rather than failing the scaffold over a cosmetic value. Closes #124 Closes #122
0b87efb to
e1d3687
Compare
|
Rebased onto main to clear the conflicts with #187 and #189. Two conflicts, both wanting both changes rather than either:
const authServerUrl = normalizeInstanceUrl(requireInstanceUrl(app));
const jwksKid = await resolveJwksKid(authServerUrl);That ordering matters and is now correct: the kid is fetched from the URL #187 resolves, so a managed instance served at
No behaviour changed in either PR's contribution. |
Closes #124
Closes #122
#122 asks a question that had to be answered before #124 could be fixed, so this closes both.
Answering #122
Traced through
seamless-auth-serverandseamless-auth-api.jwksKidreaches exactly two places, and neither verifies anything against it:Service tokens the adapter mints.
createServiceTokensignsHS256withserviceSecretand passeskeyIdas thekeyidheader. The API'svalidateInternalServiceTokenverifies withjwt.verify(token, internalSecret, { algorithms: ['HS256','HS384','HS512'] })— a symmetric shared secret. It never readskid. The header is decorative.User access tokens.
verifySignedAuthResponsebuildscreateRemoteJWKSet(new URL('/.well-known/jwks.json', authServerUrl))and hands it tojose'sjwtVerify.joseselects the key by the token header'skid. The configuredjwksKidis not passed tojwtVerifyat all.So the answer is yes, and #122's stated fear — "every login against a CLI-scaffolded app fails verification" — does not happen. Good news, and worth recording so nobody re-derives it.
But it is not harmless
warnOnDevJwksKidfires whenjwksKidis absent or equal todev-main:By writing
dev-mainexplicitly, the CLI guaranteed that warning on every managed scaffold. The developer is told their app is misconfigured, is told to set the value correctly, and has no way to find the right one, since nothing in the CLI or the portal surfaces it. That is the actual defect.The fix
The real kid is discoverable from the instance URL
initalready has.GET /.well-known/jwks.jsonreturns{ keys: [{ kid, alg: 'RS256', use: 'sig', ... }] }, so a newcore/jwksKid.tsreads the first signing key andinitwrites that.The fallback is deliberate. Since the kid verifies nothing, an instance that is still coming up should not fail a scaffold over it, so
fetchActiveJwksKidreturnsundefinedon any failure andinitkeepsdev-mainwhile saying what happened:MANAGED_JWKS_KIDis renamedFALLBACK_JWKS_KID, and its comment replaced. The old one claimed managed instances "do not expose a per-application JWKS kid", which the JWKS endpoint disproves.The printed connection values, used when there is no
api/directory and in the two post-rotation recovery paths, now show the resolved kid rather than the constant, so a developer wiring by hand gets the right value too.Verification
npm run buildandnpm testpass, 925 tests. 14 new tests: the fetcher against a well-formed set, multiple keys, an encryption key it must skip, a key omitting the optional hints, and six failure shapes; plus init writing the fetched kid, falling back with a message, and not failing the scaffold when the instance is unreadable.