Skip to content

feat(auth): add extractToken for tokens outside the Authorization bearer - #147

Merged
cuzzlor merged 2 commits into
mainfrom
feat/extract-token
Sep 30, 2026
Merged

cuzzlor merged 2 commits into
mainfrom
feat/extract-token

Conversation

@cuzzlor

@cuzzlor cuzzlor commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Some deployments authenticate with a token that is not an Authorization: Bearer token, 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:

  • useSubscriptionsServer read only the authorization / Authorization connection parameter, so verifyToken could not verify a token from anywhere else.
  • The default createUser of createSubscriptionContextFactory read the same connection parameter for User.token.
  • The default createUser of createContextFactory read only the Authorization header for User.token. With another auth header, User.token was an empty string, which breaks on-behalf-of flows.

Change

All three now accept an optional extractToken function:

Option Signature Used for
useSubscriptionsServer({ extractToken }) ExtractSubscriptionToken The token passed to verifyToken in onConnect and onSubscribe
createSubscriptionContextFactory({ extractToken }) ExtractSubscriptionToken User.token in the default createUser
createContextFactory({ extractToken }) ExtractToken User.token in the default createUser

ExtractSubscriptionToken receives { 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:

const extractToken: ExtractSubscriptionToken = ({ connectRequest }) =>
  connectRequest.headers['x-goog-iap-jwt-assertion'] as string | undefined

const createSubscriptionContext = createSubscriptionContextFactory({ requestLogger, extractToken })

useSubscriptionsServer({ schema, httpServer, logger, createSubscriptionContext, extractToken, requireAuth: true, verifyToken })

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

  • extractToken is not called when createUser is supplied. In the context factories it applies only to the default createUser. A custom createUser can call the same extractor itself. The doc comments and README say this.
  • Log message change. When requireAuth is set and no token is found, the error log now reads No auth token was supplied with the websocket connection instead of No authorization parameter was supplied via websocket connection params. Log queries or alerts that match the old text must be updated.
  • New vitest.config.ts. It inlines graphql-ws in tests. Without it, Vite resolves graphql to its ESM build for test code, while Node loads the CJS build for the externalised graphql-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 for useSubscriptionsServer): runs a real ws server with a graphql-ws client.
    • By default, the bearer token from the connection params is verified.
    • A custom extractToken that reads a connect request header is used on connect and on subscribe, and its token reaches User.token.
    • With requireAuth, the connection is rejected with close code 4403 when the extractor returns no token.
  • context.spec.ts and subscriptions/context.spec.ts: extractToken sets the default User.token, and an extractor that returns undefined gives an empty token.

Verification

  • npm test: 161 passed (the server tests also passed on 5 repeated runs)
  • npm run check-types: clean
  • npm run lint: clean
  • npm run build (rollup and attw): "No problems found"

Dependency vulnerability fixes (commit 2)

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 (lockfile only).
  • vitest and @vitest/coverage-v8 are pinned exactly, so they move from 4.1.5 to the patched 4.1.11.
  • @arethetypeswrong/cli moves from ^0.18.2 to ^0.18.5. After the fflate 0.8.3 fix, its streaming Gunzip emits several chunks. attw 0.18.2 kept only the last chunk, so build:6-check-exports failed with Cannot read properties of undefined (reading 'filename'). attw 0.18.5 joins the chunks.

After this commit, npm run audit finds 0 vulnerabilities, npm test passes all 161 tests, and npm run build (including attw) reports "No problems found".

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.
@cuzzlor
cuzzlor requested review from mderriey and a balanced review from Copilot September 30, 2026 10:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 extractToken options 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.
@cuzzlor
cuzzlor merged commit 3357068 into main Sep 30, 2026
2 checks passed
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.

3 participants