feat(fastify): proxy the admin console from the Fastify adapter - #141
Merged
Conversation
The Express adapter ships createSeamlessConsoleProxy, which reverse-proxies the admin dashboard so an adopter's API can serve it from its own origin. The Fastify adapter had no equivalent, and its README said so. seamlessConsoleProxy is a plugin registered under a prefix, the same shape as seamlessAuth, rather than a factory named after the Express function. Fastify already owns the mount path, so taking it from the options too would give two sources for one value. basePath keeps its meaning from the Express adapter: it says what to request upstream, not where the proxy is mounted. Hand-rolled on fetch rather than @fastify/http-proxy or @fastify/reply-from. The adapter already proxies to the auth server with fetch through core's proxyRequest, adding no dependency for it, and the console needs the path allowlist and the stripping of everything but the method and the path. A general proxy would have to be configured back down to that. The upstream URL is resolved from the raw request.url rather than the wildcard param. Fastify percent-decodes params, which turns %2e%2e into .. and %2f into / before any check could see the difference, and the traversal check has to look at what the client actually sent. Express reaches the same place from the other direction: it normalizes dot-segments before routing, so its version reads the already-normalized req.path. The parity suite now runs the console proxy through both adapters and asserts the status, body, caching headers and upstream URL match. The two frameworks normalize dot-segments at different points, so a traversal attempt ends in a 400 on one and a 404 on the other; that case asserts the invariant instead, that neither ever requests anything outside the console subtree. pnpm build and pnpm test pass: 214 core, 150 express, 40 fastify.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
@seamless-auth/fastifygainsseamlessConsoleProxy, the Fastify equivalent of the Express adapter'screateSeamlessConsoleProxy. An adopter's Fastify API can now serve the Seamless admin dashboard from its own origin at/console, alongside the auth routes it already serves.The README's "Not included" section, which said this had no Fastify equivalent, is gone. The section that replaces it documents the export and names the one shape difference from Express.
Why a plugin rather than a factory
seamlessConsoleProxyis aFastifyPluginAsyncyou register under a prefix, matching howseamlessAuthis registered, rather than a factory named after the Express function:Fastify already owns the mount path through
prefix. A factory would have to take the mount path in its options as well, giving one value two sources that can disagree.basePathkeeps the meaning it has in the Express adapter, which is what to request upstream rather than where the proxy is mounted, and still defaults to/console.Why hand-rolled on fetch
Not
@fastify/http-proxyor@fastify/reply-from. The adapter already proxies to the auth server withfetchthrough core'sproxyRequestand takes no dependency for it. The console proxy is not a general reverse proxy either: it allows exactly one subtree, permits onlyGETandHEAD, forwards nothing from the incoming request but the method and the path, and copies back four response headers. A general proxy would be configured back down to that, and the security-relevant part would still be hand-written.Path handling
The upstream URL is resolved from the raw
request.url, not from the wildcard param. Fastify percent-decodes params, which turns%2e%2einto..and%2finto/before any check could see the difference, and the traversal check has to look at what the client actually sent. Express arrives at the same place from the other direction: it normalizes dot-segments before routing, so its version reads the already-normalizedreq.path.Encoded path separators (
%2f,%5c) are rejected outright. They survivenew URLundecoded, so..%2fadminwould pass a prefix check here and decode to a traversal at an upstream that does decode it. Legitimate console asset paths and SPA client routes never contain one.A subpath is only trusted when
request.urlstarts with the mount path. WithignoreDuplicateSlashesthe router matches a path the raw url does not start with, and there is nothing to strip; that request is refused rather than guessed at.Tests
The parity suite now runs the console proxy through both adapters against the same mocked upstream and asserts the status, body,
content-type,cache-control, and the upstream URL match: asset request, deep client route, query string, upstream 404, a write refused with 405, both encoded-separator traversals, and an unreachable upstream.The two frameworks normalize dot-segments at different points, so a literal
..traversal ends in a 400 on one adapter and a 404 on the other. That case asserts the invariant both have to hold instead: neither ever requests anything outside the console subtree.tests/consoleProxy.test.jscovers what is Fastify's alone: the prefix root with and without a trailing slash, a custom prefix, a custombasePath,HEAD, that noCookieorAuthorizationheader reaches the upstream, and the mount-path guard.Checks
pnpm buildandpnpm testat the workspace root, on Node 24:Follow-up
Not in this PR. Once this ships,
seamless-templatescan addtemplates/api/fastifywithSERVE_ADMIN_CONSOLEsupport and the CORS handling that mirrorstemplates/api/express/src/index.ts, so the CLI's "Served by your API at /console (recommended)" default works for a Fastify project too. The changeset here is a minor on a 0.x package, so that template should pin@seamless-auth/fastifyat^0.2.0, the first release that has this export.