feat(auth): add extractToken for tokens outside the Authorization bearer - #147
Merged
Merged
Conversation
Some deployments authenticate with a token that is not an Authorization bearer token, e.g. a JWT assertion header that a proxy such as Google Cloud IAP adds to the request. useSubscriptionsServer, createSubscriptionContextFactory and createContextFactory now accept an extractToken function. The subscription extractor receives the connect request and the connection params, so it can read a header from the websocket upgrade request. Without extractToken, the bearer token is extracted as before. The "no token" error log no longer mentions connection params, because the token can now come from elsewhere.
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation is backward-compatible, consistently documented, and adequately covered by focused tests.
Review effort: Balanced
Findings: None
What changed in this PR
Adds configurable token extraction for HTTP and WebSocket contexts while preserving existing bearer-token defaults.
Changes:
- Adds
extractTokenoptions to context factories and subscription authentication. - Adds integration and unit coverage for custom extraction.
- Documents the API, configures Vitest, and bumps the minor version.
| File | Description |
|---|---|
vitest.config.ts |
Inlines graphql-ws during tests. |
src/subscriptions/utils.ts |
Adds the default subscription extractor. |
src/subscriptions/server.ts |
Uses configurable extraction for verification. |
src/subscriptions/server.spec.ts |
Tests WebSocket authentication flows. |
src/subscriptions/context.ts |
Supports extraction for default users. |
src/subscriptions/context.spec.ts |
Tests custom subscription extraction. |
src/context.ts |
Adds HTTP extractToken support. |
src/context.spec.ts |
Tests HTTP token extraction behavior. |
README.md |
Documents token extraction options. |
package.json |
Bumps the package to 3.4.0. |
package-lock.json |
Synchronizes the package version. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`npm run audit` reported 19 advisories in nine packages. None reach consumers: this package has no runtime dependencies, so every affected path is a devDependency. - `npm audit fix` resolved brace-expansion, fast-uri, js-yaml, qs, fflate and @humanfs/node within the existing semver ranges. - vitest and @vitest/coverage-v8 are pinned exactly, so they move from 4.1.5 to the patched 4.1.11. - @arethetypeswrong/cli moves to ^0.18.5. The fflate 0.8.3 fix changes its streaming Gunzip to emit several chunks, and attw 0.18.2 kept only the last one, so `build:6-check-exports` failed to read the packed tarball. attw 0.18.5 joins the chunks.
mderriey
approved these changes
Sep 30, 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.
Problem
Some deployments authenticate with a token that is not an
Authorization: Bearertoken, for example a JWT assertion header that a proxy such as Google Cloud IAP (x-goog-iap-jwt-assertion) adds to the request.The token location was hard-coded in three places:
useSubscriptionsServerread only theauthorization/Authorizationconnection parameter, soverifyTokencould not verify a token from anywhere else.createUserofcreateSubscriptionContextFactoryread the same connection parameter forUser.token.createUserofcreateContextFactoryread only theAuthorizationheader forUser.token. With another auth header,User.tokenwas an empty string, which breaks on-behalf-of flows.Change
All three now accept an optional
extractTokenfunction:useSubscriptionsServer({ extractToken })ExtractSubscriptionTokenverifyTokeninonConnectandonSubscribecreateSubscriptionContextFactory({ extractToken })ExtractSubscriptionTokenUser.tokenin the defaultcreateUsercreateContextFactory({ extractToken })ExtractTokenUser.tokenin the defaultcreateUserExtractSubscriptionTokenreceives{ connectRequest, connectionParams }, so it can read a header from the websocket upgrade request. Browsers cannot set custom headers on a websocket request, but a proxy can. The same function goes to both the server and the subscription context factory:Without
extractToken, the token is extracted as before. This is a backward-compatible feature, so the version goes from 3.3.0 to 3.4.0.Things to review
extractTokenis not called whencreateUseris supplied. In the context factories it applies only to the defaultcreateUser. A customcreateUsercan call the same extractor itself. The doc comments and README say this.requireAuthis set and no token is found, the error log now readsNo auth token was supplied with the websocket connectioninstead ofNo authorization parameter was supplied via websocket connection params. Log queries or alerts that match the old text must be updated.vitest.config.ts. It inlinesgraphql-wsin tests. Without it, Vite resolvesgraphqlto its ESM build for test code, while Node loads the CJS build for the externalisedgraphql-ws, and schema execution fails graphql's realm check. This affects only the test environment.Tests
src/subscriptions/server.spec.ts(new, the first tests foruseSubscriptionsServer): runs a realwsserver with agraphql-wsclient.extractTokenthat reads a connect request header is used on connect and on subscribe, and its token reachesUser.token.requireAuth, the connection is rejected with close code 4403 when the extractor returns no token.context.spec.tsandsubscriptions/context.spec.ts:extractTokensets the defaultUser.token, and an extractor that returnsundefinedgives an empty token.Verification
npm test: 161 passed (the server tests also passed on 5 repeated runs)npm run check-types: cleannpm run lint: cleannpm run build(rollup andattw): "No problems found"Dependency vulnerability fixes (commit 2)
npm run auditreported 19 advisories in nine packages. None reach consumers: this package has no runtime dependencies, so every affected path is a devDependency.npm audit fixresolvedbrace-expansion,fast-uri,js-yaml,qs,fflateand@humanfs/nodewithin the existing semver ranges (lockfile only).vitestand@vitest/coverage-v8are pinned exactly, so they move from 4.1.5 to the patched 4.1.11.@arethetypeswrong/climoves from ^0.18.2 to ^0.18.5. After thefflate0.8.3 fix, its streamingGunzipemits several chunks. attw 0.18.2 kept only the last chunk, sobuild:6-check-exportsfailed withCannot read properties of undefined (reading 'filename'). attw 0.18.5 joins the chunks.After this commit,
npm run auditfinds 0 vulnerabilities,npm testpasses all 161 tests, andnpm run build(includingattw) reports "No problems found".