fix(portal-bff): serve /.well-known/jwks.json via express (path-to-regexp v8 ducks the dot)
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.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
// OpenTelemetry auto-instrumentations and is silently un-traced.
|
||||
import './observability/tracing';
|
||||
|
||||
import { RequestMethod, ValidationPipe } from '@nestjs/common';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import helmet from 'helmet';
|
||||
@@ -21,6 +21,7 @@ import { assertSessionSecret } from './config/check-session-secret';
|
||||
import { createRateLimitMiddleware, readRateLimitConfig } from './security/rate-limit.middleware';
|
||||
import { CSRF_MIDDLEWARE } from './security/security.token';
|
||||
import { StructuredErrorFilter } from './security/structured-error.filter';
|
||||
import { JwksPublisher } from './downstream/jwks.publisher';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import {
|
||||
ADMIN_SESSION_MIDDLEWARE,
|
||||
@@ -191,14 +192,27 @@ async function bootstrap() {
|
||||
app.use(app.get<RequestHandler>(CSRF_MIDDLEWARE));
|
||||
|
||||
const globalPrefix = 'api';
|
||||
// `/.well-known/*` is reserved by RFC 8615 for bare-root metadata
|
||||
// endpoints. The BFF's JWKS controller (ADR-0014 signed-assertion
|
||||
// strategy) lives at `/.well-known/jwks.json` so downstream
|
||||
// services pointing at the standard location find it. Excluding
|
||||
// the prefix lets Nest's router resolve the route at the root.
|
||||
app.setGlobalPrefix(globalPrefix, {
|
||||
exclude: [{ path: '.well-known/jwks.json', method: RequestMethod.GET }],
|
||||
app.setGlobalPrefix(globalPrefix);
|
||||
|
||||
// JWKS endpoint at the RFC 8615 bare-root path. Wired at the
|
||||
// Express layer rather than as a Nest `@Controller` because
|
||||
// path-to-regexp v8 (Nest 11's router) does not cleanly route a
|
||||
// leading-dot segment like `.well-known/jwks.json` to a bare-root
|
||||
// URL — the previous Nest-side attempt with `setGlobalPrefix`
|
||||
// `exclude` landed the route at neither `/api/.well-known/jwks.json`
|
||||
// nor `/.well-known/jwks.json` (both 404'd). Express's own
|
||||
// routing accepts the leading dot verbatim, and the Nest DI
|
||||
// container still owns the underlying `JwksPublisher` service.
|
||||
//
|
||||
// Public by design (no session, no CSRF) — the JWKS is the
|
||||
// downstream's verification anchor; gating it defeats the
|
||||
// purpose. Mounted before `app.listen()` so the route is live
|
||||
// by the time the BFF reports ready.
|
||||
const jwksPublisher = app.get(JwksPublisher);
|
||||
app.getHttpAdapter().get('/.well-known/jwks.json', (_req: Request, res: Response) => {
|
||||
res.json(jwksPublisher.jwks());
|
||||
});
|
||||
|
||||
const port = process.env['PORT'] ?? 3000;
|
||||
await app.listen(port);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user