Skip to content

fix(init): write the signing key id a managed instance publishes - #191

Merged
Bccorb merged 1 commit into
mainfrom
fix/managed-jwks-kid
Sep 5, 2026
Merged

fix(init): write the signing key id a managed instance publishes#191
Bccorb merged 1 commit into
mainfrom
fix/managed-jwks-kid

Conversation

@Bccorb

@Bccorb Bccorb commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #124
Closes #122

#122 asks a question that had to be answered before #124 could be fixed, so this closes both.

Answering #122

confirm the SDK resolves the key by token-header kid (and if so, drop the misleading hardcoded value or make it clearly a non-op for managed)

Traced through seamless-auth-server and seamless-auth-api. jwksKid reaches exactly two places, and neither verifies anything against it:

Service tokens the adapter mints. createServiceToken signs HS256 with serviceSecret and passes keyId as the keyid header. The API's validateInternalServiceToken verifies with jwt.verify(token, internalSecret, { algorithms: ['HS256','HS384','HS512'] }) — a symmetric shared secret. It never reads kid. The header is decorative.

User access tokens. verifySignedAuthResponse builds createRemoteJWKSet(new URL('/.well-known/jwks.json', authServerUrl)) and hands it to jose's jwtVerify. jose selects the key by the token header's kid. The configured jwksKid is not passed to jwtVerify at 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

warnOnDevJwksKid fires when jwksKid is absent or equal to dev-main:

[SEAMLESS-AUTH-EXPRESS] - jwksKid is not set and defaults to "dev-main".
Set jwksKid explicitly to the active JWKS key id before deploying.

By writing dev-main explicitly, 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 init already has. GET /.well-known/jwks.json returns { keys: [{ kid, alg: 'RS256', use: 'sig', ... }] }, so a new core/jwksKid.ts reads the first signing key and init writes 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 fetchActiveJwksKid returns undefined on any failure and init keeps dev-main while saying what happened:

Could not read the signing key id from https://... , so JWKS_KID is set to "dev-main".
  Nothing verifies against it, the SDK resolves the key from the token, but your adapter
  will warn on boot until it matches. Read it from /.well-known/jwks.json once the
  instance is up.

MANAGED_JWKS_KID is renamed FALLBACK_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 build and npm test pass, 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.

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
@Bccorb
Bccorb force-pushed the fix/managed-jwks-kid branch from 0b87efb to e1d3687 Compare September 5, 2026 19:03
@Bccorb

Bccorb commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main to clear the conflicts with #187 and #189.

Two conflicts, both wanting both changes rather than either:

src/commands/init.ts (twice, once per managed scaffold path). #187 changed the URL source to requireInstanceUrl(app); this branch added the kid resolution on the following line. Resolved by keeping both:

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 domain/<infraId> is asked at the right place. Before the rebase this branch would have fetched from the stale domain.

src/commands/init.test.ts. Both PRs appended a describe at the end of the file with a nearly identical managedRun helper. Rather than carry two copies, the helper is lifted out once with an optional over parameter, and both blocks use it. #187's tests pass an app patch, mine call it bare.

No behaviour changed in either PR's contribution. npm run build and npm test pass, 991 tests, and all six tests across the two describe blocks run and pass.

@Bccorb
Bccorb merged commit 6976422 into main Sep 5, 2026
3 checks passed
@Bccorb
Bccorb deleted the fix/managed-jwks-kid branch September 5, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: stop hardcoding JWKS_KID dev-main for managed scaffolds Managed init hardcodes JWKS_KID=dev-main, may not match the instance signing kid

1 participant