Repository navigation
fix: guard unsafe requests whatever the SameSite setting, and no refresh replay after rotation - #270
Merged
Merged
Conversation
A same-site or fetch-metadata-free form POST carrying a Lax access cookie reaches cookie-authenticated routes on Express, Fastify (text/plain or no content type) and Next.js, and checkOrigin ignores cross-site and allowedOrigins unless cookies are SameSite=None. These tests fail on main. Refs #256
The origin check only ran for SameSite=None cookies, and a body-less form
POST reached cookie-authenticated routes because Express and Next.js
forward urlencoded and multipart as an empty body and Fastify accepts
text/plain and a missing content type.
checkOrigin now runs whatever the SameSite setting and enforces
allowedOrigins (an empty list included) for same-site and metadata-free
requests. A new checkContentType refuses a state-changing request to the
auth routes with 415 unsupported_media_type unless it declares
application/json or carries an x-seamless-auth-transport header, even with
no body. POST /refresh, DELETE /logout and DELETE /logout/all are exempt
for now because the current browser client sends them bare.
Fastify accepts an empty JSON body on the plugin's routes and Express no
longer reads one as {}, so a body-less JSON request forwards no body.
Refs #256
A second caller presenting the same refresh cookie, session token or bearer refresh token after a completed rotation gets the new session from the 5 second completed-result window, and the auth API is never called. These tests fail on main. Refs #258
dedupeRefresh kept a completed rotation for 5 seconds and handed it to any caller presenting the same refresh cookie, session token or bearer refresh token, without calling the auth API. A stolen refresh token replayed in that window got the new session, and the API's reuse detection never saw it. Drop the completed-result window for every transport. Callers that race one rotation still share it; a caller that arrives after it completed goes to the auth API as a replay. Refs #258
Express and Fastify requireAuth let a cross-site, unlisted same-site or null-Origin state change through on a cookie session, where the Next.js guard refuses it. checkOrigin also passes a literal null Origin when Sec-Fetch-Site is same-origin, same-site or none. These tests fail on the branch. Refs #256
requireAuth in Express and Fastify never called checkOrigin, so a cross-site, unlisted same-site or null-Origin state change on a cookie session reached the adopter's route, where the Next.js guard and the Go, Python and Rust adapters refuse it. Run the origin check on a cookie session, before authentication, with a new allowedOrigins option. Bearer requests are not checked and no content type is required. checkOrigin now refuses a literal null Origin whatever Sec-Fetch-Site and allowedOrigins say, instead of only when Sec-Fetch-Site is absent. Refs #256
The Fastify and Next.js option tables gain the 415 gate, its exemptions and the null-Origin rule, matching the Express README. The Express and Fastify guard sections describe the origin check and allowedOrigins. Refs #256
…ing proxy setting Refs #257
…ding the client IP Probe the compiled trust proxy function with documentation addresses, as the Fastify adapter does, so subnets covering every address or a function that always trusts no longer let a caller choose x-seamless-client-ip. Refs #257
A verified user now carries isClient, true when sub equals client_id, so adopters can tell a machine acting for itself from a user before reading id as a user id. Refs #260
Adds shared vectors for isClient on verified tokens and for an auth guard that refuses client credentials tokens with 403 user_required. Both are additive: is_client sits beside each verify entry's user, and the new requests live in their own user_required_requests list, so the Go, Python and Rust copies keep passing until those adapters take them up. Refs #260
requireAuth from createResourceServer (and the Next.js requirements, and core authenticate) take allowClients. With false, a verified client credentials token is refused with 403 user_required and no challenge, the same shape as insufficient_role. Invalid tokens still get 401 first, and the default keeps today's behavior. Refs #260
This was referenced Nov 3, 2026
… content-type gate
…sh cookie Browser tabs share one cookie jar, so a second tab can send the old refresh cookie a moment after the first tab's rotation completed. That replay should get the session the first tab got for 5 seconds, without calling the auth API. After 5 seconds, or for a failed rotation, it goes to the auth API. A bearer replay always goes to the auth API, even inside the 5 seconds. The two cookie window tests fail on this branch. Refs #258
Browser tabs share one refresh cookie, so a second tab can send the old cookie just after the first tab's rotation completed. Sending it to the auth API trips reuse detection and signs both tabs out, which breaks the multi-tab conformance case. Keep a successful cookie rotation for 5 seconds, keyed on a hash of the old refresh cookie, and hand it to a caller presenting that cookie without calling the auth API. Failed rotations are not kept, expired entries are pruned on insert and the map is capped. Bearer and session token refreshes keep sharing only a rotation in flight, so a late bearer replay still reaches the auth API. Refs #258
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.
Fixes two v0.21 security review findings (epic fells-code/seamless-auth-api#558) in core, Express, Fastify and Next.js. Each was reproduced on
mainfirst:1f64072,00468d9and09f599badd only tests, which fail there.Closes #256
Closes #258
Origin and content type (#256)
allowedOrigins(with[]strict) applies tosame-siterequests and to requests with no fetch metadata that carry an Origin.Origin: nullis refused first, in every case. A request with neitherOriginnorSec-Fetch-Sitepasses the origin check.checkContentType: a body-less unsafe auth request needsContent-Type: application/jsonor anx-seamless-auth-transportheader (any value), otherwise 415unsupported_media_type.POST /refresh,DELETE /logoutandDELETE /logout/allare exempt in v0.23 and still get the origin check. Origin (403) runs before the gate (415), both before cookie refresh and routing. A refused logout clears nothing.text/plainand no content type. Its plugin now reads an empty JSON body as no body, and Express forwards an empty JSON body as no body rather than{}.requireAuth/requireRolenow run the origin check on cookie sessions, matching Next.js, Go, Python and Rust. NewallowedOriginsoption onrequireAuth. Guards get the origin check only, not the content-type gate.Refresh replay (#258)
The completed-result cache in
dedupeRefreshis gone. Only in-flight rotations are shared, for every transport. A refresh token presented after its rotation finished reaches the auth API, which treats it as reuse.This is the cross-adapter contract recorded in the v0.23 plan (fells-code/fellscode#32), matching fells-code/seamless-auth-go#30, fells-code/seamless-auth-python#33 and fells-code/seamless-auth-rust#34.
Merge order
Ready:
@seamless-auth/client0.8.3 (fells-code/seamless-auth-js#234) and seamless-cli 0.27.2 (fells-code/seamless-cli#345), which sendx-seamless-auth-transporton unsafe requests, are released. Conformance runs withSEAMLESS_VERIFY_CONTRACTS=content-type-gate, so the 415 case is enforced here.Refresh sharing was revised on 2026-11-03: a completed rotation is kept for 5 seconds for cookie transport only (concurrent tabs keep their session), and bearer replays always reach the auth API.
Release notes
Origin: null, and unlisted same-site unsafe requests get 403cross_site_request_blocked, on the auth routes and on guarded adopter routes with a cookie session.Checks
pnpm buildclean.pnpm test: core 688, express 300, fastify 304, nextjs 334, all passing. The conformance suite runs in CI.Also in this PR (adapter-specific findings)
Closes #257
Closes #259
Refs #260 (partly)
30f1301test,a63c4f6fix): Express now asks its compiledtrust proxy fnwhether it trusts RFC 5737 documentation addresses at hops 0, 1 and 99, as the Fastify adapter does. Any trust-everything setting (() => true, or the whole address space as two/1ranges; Express refuses0.0.0.0/0itself) forwards no client IP and logs a warning. Hop counts and named proxies forward as before. A broad subnet that misses the probes (for example10.0.0.0/8) is not flagged, matching Fastify.0ccda2dadds a regression test (a key served at the host root must not verify a prefixed server's token); with the old URL code restored it fails.verifyUpstreamSession(..., "ephemeral")), with existing tests that fail on the pre-fix(core): align upstream calls with the other adapters #267 code.isClient(cdab867test,1fc3b9cfix):ResourceServerClaimsgainsisClient, true whensub === clientId. It is a new required field, so hand-builtResourceServerUserdoubles must add it. Still open: a guard option that requires a user rather than a client, which needs a cross-adapter decision (see the issue).