Repository navigation
fix: v0.20 adapter hardening (#225 to #231) - #234
Merged
Merged
Conversation
encodeURIComponent leaves `.` and `..` unchanged, so a decoded `%2e%2e` route param moved the upstream path on the hand-written passthroughs. Core now has encodePathParam, which refuses empty and dot-segment values with InvalidPathParamError (400), shared with the manifest matcher. Express, Fastify, Next.js and the core admin, session, organization switch, magic link and OAuth handlers all build upstream paths with it. Closes #225
…ssion Every adapter cookie is signed with the same secret, so ensureCookies accepted any signed cookie with a token string in the access slot. It now requires the inner token to be an access token, reusing holdsAccessToken from the guards. verifyUpstreamSession checks the verified token's typ: access for session flows, ephemeral for /login and the manifest's pre-auth and registration routes. Tests that put opaque strings in access cookies now use typed tokens. Closes #226
authFetch used fetch's default redirect: "follow", so a redirect from a misconfigured proxy in front of the API would resend the service token and forwarded client headers to another origin. Every upstream request now uses redirect: "manual", and a 3xx becomes a 502 upstream_redirect error. The manifest fetch also refuses to follow a redirect and uses the bundled copy. Closes #227
…console proxy The console proxy forwarded only Content-Type, Cache-Control, ETag and Last-Modified, so the API's X-Frame-Options, Content-Security-Policy and X-Content-Type-Options were dropped and the admin console served on the app origin could be framed. All three adapters now forward them, and fall back to X-Frame-Options: DENY and X-Content-Type-Options: nosniff when neither the API nor the application set them. Closes #228
… present A present Sec-Fetch-Site short-circuited the origin guard and same-site was allowed, so allowedOrigins only applied to browsers that omit the header. With SameSite=None cookies a sibling subdomain counts as same-site. When allowedOrigins is configured, checkOrigin now also requires a listed Origin for a state-changing same-site request. same-origin still passes, cross-site is still refused, and the default without allowedOrigins is unchanged. The READMEs document it. Closes #230
assertSecretStrength checked only length, so the starters' example cookie secret passed it. It now refuses the published placeholders and anything containing replace-me. assertSecrets reads cookieSecure, so the Express, Fastify and Next.js adapters refuse one at startup unless they run in insecure local mode. Per-request and guard checks keep the length check only, since they cannot tell local mode apart. As defense in depth, authenticateRequest verifies a cookie's inner access token against the auth API's JWKS when the guard has authServerUrl and audience, and takes the roles from that token. Closes #231
Every workflow now sets a top-level permissions: contents: read, and the release and prerelease jobs keep their own wider grants for publishing. Actions are pinned to full commit SHAs with a version comment, including changesets/action in the publishing job, and Dependabot keeps the github-actions pins current. Publishing still uses NPM_TOKEN: moving to npm trusted publishing needs each package configured on npmjs.com first. Refs #229
This was referenced Oct 25, 2026
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.
v0.20 hardening for the JS server adapters (core, Express, Fastify, Next.js). One commit per issue.
Fixes
#225 Refuse dot-segment params in hand-written passthroughs
encodePathParamandInvalidPathParamError(400). They refuse empty,.and..params, sharing the dot-segment rule (isDotSegment) with the manifest matcher.resolveUpstreamPath, and the core admin, session, organization switch, magic link verify and OAuth handlers. Next.js maps the error to400 bad_request; Express and Fastify already did that for a 4xx error.packages/fastify/tests/dotSegmentParams.test.jssends raw%2e%2epaths over a real socket to both Fastify and Express (light-my-request and supertest resolve dot segments themselves).packages/core/tests/pathParam.test.jscovers the helper and the core handlers. Next.js parses every request URL with the WHATWG parser, which already resolves%2e%2e, so it gets the shared core check but no route-level test.#226 Check the token type in ensureCookies and verifyUpstreamSession
ensureCookiesnow requires the access cookie's inner token to betyp: "access", using the guards'holdsAccessToken(now exported). Otherwise it answers 401 and clears the cookie, as for an invalid one.verifyUpstreamSessioncheckstyp. It takes a new optionalexpectedTypeargument (defaultaccess)./loginand the manifest's pre-auth and registration routes passephemeral."access-token"in access cookies now use typed tokens.#227 Do not follow redirects on upstream requests
authFetchsends every upstream request withredirect: "manual", and a 3xx becomes502 { "error": "upstream_redirect" }with one error log. The log names only the origin, because the path can carry a magic link token. The manifest fetch also usesredirect: "manual"and falls back to the bundled copy.#228 Keep the API's framing and content-type headers on the console proxy
Content-Security-Policy,X-Frame-OptionsandX-Content-Type-Options. When neither the API nor the application set the last two, the proxy falls back toX-Frame-Options: DENYandX-Content-Type-Options: nosniff.#229 Least-privilege tokens and pinned actions
permissions: contents: read. Release (contents/pull-requests/id-token: write) and prerelease (id-token: write) keep their job-level grants.actions/checkout,actions/setup-node,pnpm/action-setupandchangesets/actionare pinned to full commit SHAs, with a version comment. Each stays on its current major..github/dependabot.ymladds weekly, groupedgithub-actionsupdates.NPM_TOKENcan go, so publishing still uses the token and stays working. The reusable conformance workflow stays onfells-code/seamless-cli@main(first-party).#230 Check Origin against allowedOrigins even when Sec-Fetch-Site is present
checkOrigin:cross-siteis refused andsame-originpasses. WithallowedOriginsconfigured, a state-changingsame-siterequest must now carry anOriginon the list (a missingOriginis refused).allowedOrigins, behavior is unchanged (asame-siterequest passes). This is documented in the Express, Fastify and Next.js READMEs.#231 Refuse known placeholder secrets at startup
assertSecretStrengthrefusesdev-only-cookie-signing-key-replace-me,GRAB_FROM_SEAMLESS_AUTH_PORTAL_DEVand anything containingreplace-me. It takes a new{ allowPlaceholder }option.assertSecretsreadscookieSecure, so the Express router, the Fastify plugin and the Next.js handler refuse a placeholder at startup unlesscookieSecure: false(the insecure local mode).ensureCookies, refresh,getSeamlessUser,createServiceToken,requireAuth, the Next.js session helpers) keep the length check only. They cannot tell local mode apart, and the adapter has already refused at startup.authenticateRequest(behindrequireAuthin Express and Fastify) now verifies a cookie's inner access token against JWKS when the guard hasauthServerUrlandaudience, requires a matchingsub, and takes the roles from the verified token. Without that configuration there is no JWKS to check against, so it still only decodes. The syncauthenticateCookieand the Next.jsgetSeamlessClaimskeep their local-only contract.getSeamlessSessionsends the token to the API, which verifies it.Changesets
Patch for #225, #226, #227 and #228. Minor (pre-1.0 signal for a behavior change) for #230 and #231. Each covers core and the adapters it touches. None is major.
Checks (worktree root)
pnpm check:types-currentfails onmainas well:@seamless-auth/types0.31.0 is published and the packages pin^0.29.0. That bump is a separate change, so it is not part of this PR.Cross-repo notes
upstream_redirect, 502) that SDKs may see.Closes #225
Closes #226
Closes #227
Closes #228
Refs #229
Closes #230
Closes #231