fix(portal-bff): serve /.well-known/jwks.json via express (path-to-regexp v8 ducks the dot) #139
Reference in New Issue
Block a user
Delete Branch "fix/portal-bff-jwks-express-route"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The Nest
@Controller('.well-known/jwks.json')declared in PR #138 combined withsetGlobalPrefix('api', { exclude: [...] })landed the JWKS route at neither/.well-known/jwks.json(intended) nor/api/.well-known/jwks.json(with-prefix fallback). Both URLs 404'd. The user reported it on the merged PR; this fix reroutes the endpoint so the JWKS lands at the correct RFC 8615 bare-root path.Root cause
Nest 11 routes via path-to-regexp v8.4.2, whose grammar broke backward compatibility on several leading-character cases. The combination of a leading-dot path segment (
.well-known) plus thesetGlobalPrefixexcluderewrite falls into one of those cases — the route registers but matches no incoming request. Without theexclude, it would register under/api/.well-known/jwks.json, which would at least be reachable, but withexcludeenabled it ends up in a path-to-regexp limbo.Fix
Sidestep Nest's router for this one route. The JWKS payload-builder stays in the Nest DI graph (renamed
JwksController→JwksPublisher, just the decorators stripped), andmain.tsresolves it from the container then registers a plain Express GET handler at/.well-known/jwks.json. Express's router accepts the leading dot verbatim and the route lands exactly where RFC 8615 says it should.Touched
jwks.controller.{ts,spec.ts}→jwks.publisher.{ts,spec.ts}. Same constructor, samejwks()method shape — only the@Controller/@Getdecorators are gone. The DI signature is unchanged so the existing tests rename → green without other edits.downstream.module.ts: drops thecontrollersarray, listsJwksPublisheras a provider + export somain.tscan resolve it.main.ts: drops thesetGlobalPrefixexcludeoption, drops theRequestMethodimport, registers an Express GET handler at the bare-root JWKS path immediately beforeapp.listen().Verification
Verified locally against a running BFF (with a generated RSA-3072 key +
BFF_JWKS_KID=bff-2026-05):Test plan
pnpm nx test portal-bff— 358 specs pass (unchanged: the publisher'sjwks()method shape is identical, the rename-only spec delta keeps the existing coverage).pnpm exec nx affected -t format:check lint test build --base=origin/main— clean.curl http://localhost:3000/.well-known/jwks.jsonreturns the JWKS with the configuredkid,alg=RS256,use=sig. No private RSA components (d/p/q/dp/dq/qi) in the response.Notes for the reviewer
main.ts. Production behaviour is identical to a Nest-routed controller; only the registration mechanism differs.TestingModulewouldn't exercise the actual Express route binding either, so the manual curl + the publisher's existing unit tests are the right coverage.The Nest `@Controller('.well-known/jwks.json')` declared in PR #138 combined with `setGlobalPrefix('api', { exclude: [...] })` landed the JWKS route at neither `/.well-known/jwks.json` (intended) nor `/api/.well-known/jwks.json` (with-prefix fallback). Both URLs 404'd. Nest 11 routes via path-to-regexp v8, whose grammar broke backward compatibility on several leading-character cases — the combination of a leading-dot path segment + the `exclude` rewrite falls into one of them. Fix Sidestep Nest's router for this one route. The JWKS payload-builder stays in the DI graph (`JwksPublisher`, formerly `JwksController`, minus the Nest decorators), and `main.ts` resolves it from the container then registers a plain Express GET handler at `/.well-known/jwks.json`. Express's router accepts the leading dot verbatim and the route lands exactly where RFC 8615 says it should. Touched - jwks.controller.{ts,spec.ts} → jwks.publisher.{ts,spec.ts}. Same constructor, same `jwks()` method shape — only the @Controller / @Get decorators are gone. The DI signature is unchanged so the existing tests rename → green without other edits. - downstream.module.ts: drops the `controllers` array, lists `JwksPublisher` as a provider + export so `main.ts` can resolve it. - main.ts: drops the `setGlobalPrefix` exclude option, drops the `RequestMethod` import, registers an Express GET handler at the bare-root JWKS path immediately before `app.listen()`. Verified locally: `curl http://localhost:3000/.well-known/jwks.json` returns the expected JWKS shape (`kty=RSA`, `kid=bff-2026-05`, `alg=RS256`, `use=sig`). Tests: still 358 specs passing. No new specs added — the routing fix is a wiring change tested manually with the real BFF; the publisher's `jwks()` method is unchanged so the rename-only spec delta keeps the existing coverage.