Skip to content

feat(verify): an adapter conformance suite any server adapter can run - #251

Merged
Bccorb merged 1 commit into
mainfrom
feat/adapter-conformance-suite
Oct 8, 2026
Merged

Bccorb merged 1 commit into
mainfrom
feat/adapter-conformance-suite

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Closes #246. Part of fells-code/seamless-auth-api#371.

Why

Server adapters hold the auth API's tokens in cookies, so each one is part of the security surface. Only the Node adapters had parity tests, inside seamless-auth-server. The adapter specs here covered the happy paths and nothing about cookies, refresh, bearer transport or what reaches the API. A Go, Rust or Python adapter had nothing to prove itself against.

What changes

The contract. verify/CONFORMANCE.md states what a reference app on any adapter must serve:

  • GET / for health
  • the adapter at /auth
  • GET /api/me behind the adapter's own guard
  • GET /__captured/:recipient, fed by the adapter's external-delivery hook
  • one trusted proxy hop

It also gives the configuration, including AUTH_SERVER_ISSUER for an app that reaches the API through a published port, and every behaviour the specs check.

Six new specs in verify/harness/adapter/. They run against the Express and Fastify reference apps on every seamless verify.

Spec Checks
cookieAttributes HttpOnly; Secure; SameSite=None; Path=/ and Max-Age equal to the API's lifetimes; clearing on logout; no token or refreshToken in any cookie-transport body
refreshRotation Sign-in from the refresh cookie alone with both cookies rotating; concurrent requests on one refresh cookie all succeed; a used refresh cookie is refused after the 5 second reuse window; POST /auth/refresh returns no tokens
bearerTransport Tokens in bodies and no cookies; the access token accepted by /auth/users/me and /api/me; refresh rotation and reuse; logout ends the session
protectedEndpoint /api/me refuses no session and an altered cookie, and sees the signed-in user
errorPassthrough A validation 400 keeps its body; no session answers 401 with an error code; an unknown route answers 404
clientForwarding The auth API records the browser's address and user agent against the user's events, read back through the admin API

seamless verify --adapter-url=<url> starts only Postgres and the auth API, then runs the same specs as a new conformance Playwright project against a reference app the caller started. It skips packing the Node adapters and the browser layer, and exits 1 when the app fails.

Reference apps

  • They trust one proxy hop, and each virtual user arrives with its own X-Forwarded-For and User-Agent. That is what makes forwarding checkable, and it gives each user its own rate-limit bucket on the API, which removes the shared per-IP OTP cap that used to constrain these specs.
  • Fastify uses a one-hop trust function, because Fastify 5 ignores a numeric trustProxy.
  • Both serve /api/me and accept AUTH_SERVER_ISSUER.
  • They pin @seamless-auth/express ^0.19.1 and @seamless-auth/fastify ^0.10.1. The old ^0.16.0 and ^0.7.0 pins meant the daily released smoke test was testing adapters several minors old.

Found by the suite

Before merging

Checks

  • npm test: 57 files, 1175 passed. That includes 4 new verify tests for --adapter-url: services started, project and environment, no packing with --local, exit 1 on failure, and URL validation.
  • npm run build passes.
  • Real stack, with the API from main and the adapters built from fix(fastify): read the session cookie on routes outside the plugin seamless-auth-server#209:
    • adapter and adapter-fastify projects: 48 of 48, three runs in a row.
    • conformance project against the Express reference app running on the host through AUTH_SERVER_ISSUER, the setup an external adapter uses: 24 of 24.

Not covered yet: a non-JSON error body from the API, such as a plaintext 429. It needs a fault-injecting proxy between the adapter and the API, and CONFORMANCE.md says so.

Server adapters hold the auth API's tokens in cookies, so each one is part
of the security surface, but only the Node adapters had anything to prove
themselves against, and the adapter specs covered the happy paths alone.

verify/CONFORMANCE.md now states the reference app contract, and the adapter
specs cover cookie attributes and clearing, refresh rotation, concurrent
refresh and refresh-token reuse, bearer transport, the adapter's guard on an
app route, error passthrough, no tokens in cookie-transport bodies, and
forwarding of the client address and user agent, read back from the auth
API's own events. `seamless verify --adapter-url=<url>` runs them against a
reference app on any other adapter.

The reference apps trust one proxy hop, so each virtual user gets its own
client address and rate-limit bucket, serve a guarded /api/me, accept
AUTH_SERVER_ISSUER, and pin the current adapter releases.

Closes #246.
@Bccorb
Bccorb merged commit f5b42a0 into main Oct 8, 2026
4 of 5 checks passed
@Bccorb
Bccorb deleted the feat/adapter-conformance-suite branch October 8, 2026 13:39
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(verify): extract a language-agnostic adapter conformance suite

1 participant