Skip to content

feat: deliver enrollment invites through adopter messaging, and keep the session when a refresh fails transiently - #273

Merged
Bccorb merged 4 commits into
mainfrom
feat/invites-delivery-and-refresh-failures
Nov 5, 2026
Merged

Bccorb merged 4 commits into
mainfrom
feat/invites-delivery-and-refresh-failures

Conversation

@Bccorb

@Bccorb Bccorb commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Two changes, each with its tests committed first (239b132 and dc5e8a7 fail on main: 3 and 15 failures per adapter in the Fastify and Next.js parity suites).

Closes #183

Enrollment invites through adopter messaging (#183)

POST /admin/enrollment/invites is served from the manifest in all three adapters, so the change is in core (isEnrollmentInvites in manifestProxy.ts, deliverEnrollmentInvites in deliverAuthMessage.ts).

  • With messaging configured, the request carries x-seamless-auth-delivery-mode: external and the delivery service token (subject seamless-auth-external-delivery). Each results[].delivery goes through deliverAuthMessage and is removed from the response, so addresses and links never reach the browser.
  • A delivery that throws turns that entry into { status: "skipped", reason: "delivery_failed" } (sent down, skipped up). The other recipients still send and the route answers 200.
  • Without messaging the route is unchanged.
  • The API marks a recipient invited before it returns, so retrying a failed one within a day reports recently_invited (noted in the changeset).

Refresh failures keep the session (parity with fells-code/seamless-auth-go#22)

Cookies are now cleared only when the auth API answers the refresh with 401, or the refresh cookie cannot be read. New refreshCookieSession in core reports how a rotation ended; ensureCookies and the refresh handler use it.

Case Silent refresh POST /refresh (cookie) Cookies
API answers 401, or cookie unreadable 401 Refresh failed (as before) 401 invalid_refresh_token (as before) cleared
Transport error 502 upstream_unavailable 502 upstream_unavailable kept
Other non-2xx status and body pass through status and body pass through kept
Rotated session fails verification, or 2xx with no session 502 upstream_unavailable 502 (was 500 internal_error) kept

In-flight sharing and the cookie-only 5 second window are unchanged. One caller's abort cannot cancel a shared rotation in Node (only AbortSignal.timeout signals exist and the shared promise lives in a module-level map); a new Next.js test aborts the first caller mid-rotation and the second still gets 200.

Consequence, same as Go: if the rotated token fails verification (for example a wrong authServerIssuer), the API has already spent the old refresh token, so the next attempt hits reuse detection and signs out. A misconfiguration shows as one 502 and then a sign-out.

Checks

pnpm build; pnpm test: core 700, express 310, fastify 330, nextjs 361. One Express test (proxyQueryForwarding, route param encoding) failed once in a full run and passed on two reruns; it is unrelated.

Parity scenarios for POST /admin/enrollment/invites across Express, Fastify
and Next.js: the external delivery header and delivery service token when
messaging is set, one message per results[].delivery with the payloads
stripped, a failed delivery reported as delivery_failed while the rest
send, and plain passthrough without messaging. Fails until #183 lands.
With messaging configured, POST /admin/enrollment/invites now requests
external delivery with the external delivery service token and sends each
results[].delivery through deliverAuthMessage. The payloads are stripped
from the response. A recipient whose delivery throws is reported with the
API's delivery_failed skip reason and the counts are adjusted, while the
other recipients are still sent.

The route is served from the manifest in every adapter, so the handling
lives in core's handleManifestRoute.

Closes #183
Parity scenarios for the silent refresh and cookie-transport POST /refresh
in Express, Fastify and Next.js. A transport error, a non-401 answer, a
200 without a session and a rotated session that fails verification all
clear the session cookies today (POST /refresh answers 500 for the last
two). The auth API answering 401 and an unreadable refresh cookie still
clear them. The existing forged-token scenarios now expect 502 with the
cookies kept.

A Next.js scenario pins that one caller aborting its request does not
cancel a rotation another caller shares; it passes already, since no
request's abort signal reaches the upstream call.

Refs fells-code/seamless-auth-go#22
The silent refresh and cookie-transport POST /refresh cleared the session
cookies on any failure. They are now cleared only when the auth API
answers the refresh with 401 or the refresh cookie cannot be read. A
transport error answers 502 upstream_unavailable, any other non-2xx
answer passes through, and a rotated session that fails verification (or
a 2xx without one) answers 502, all with the cookies kept. This matches
the Go adapter's fix for fells-code/seamless-auth-go#22.

Core gains refreshCookieSession, which reports how a cookie rotation
ended; refreshAccessToken keeps its signature. In-flight sharing and the
5 second completed-rotation window are unchanged.

Tests that pinned the old answers are updated, and the cookie security
tests now serve a verifiable rotated token, since they had been reading
the attributes of cookies cleared by a failed verification.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: deliver enrollment invites through adopter messaging

1 participant