Skip to content

GraphQL queries bypass the per-route scope their REST equivalents require #618

Open

Description

@Miracle656

Background

Mercurius registers /graphql itself, with no Fastify route config:

await app.register(mercurius, {
  schema, resolvers, graphiql: true,
  path: '/graphql',
  …
})

So routeConfig.scope is undefined for every GraphQL request. The API-key hook still demands a key — /graphql is not marked public, so an unauthenticated request is rejected — but the scope loop has nothing to check:

const requiredScopes = routeConfig.scopes ?? (routeConfig.scope ? [routeConfig.scope] : [])
for (const scope of requiredScopes) { … }   // empty, so never runs

Meanwhile the REST routes serving the same data each declare one:

GraphQL field REST equivalent Scope REST requires
getPrice /price/:assetA/:assetB read:prices
getBestRoute /price/:assetA/:assetB/route read:prices
getPriceHistory /candles/:assetA/:assetB read:history
oracle /compare/:asset read:prices
screener /screener read:prices
priceUpdated (subscription) /ws subscribe

A key issued with only read:prices can read history through getPriceHistory; a key issued with no scopes at all can read everything. Scopes are the mechanism by which a key is restricted, so on this route they currently do nothing.

This is not new — it has been true since GraphQL was added — but it widens every time a resolver is added, most recently screener in #609.

What to build

Enforce the same scope per GraphQL field as its REST equivalent. The shape that avoids a second list to keep in sync is to put the requirement next to the resolver and check it in one place, rather than hand-maintaining a field→scope map somewhere else:

  • a per-field scope declaration (a wrapper around each resolver, or a Mercurius preExecution hook that reads the selected fields),
  • resolved against req.apiKey.scopes with hasScope from src/api/auth.ts — the same function the REST hook uses, so the two cannot disagree about what a scope means,
  • a 403 insufficient_scope shaped like the REST one, surfaced through formatGraphQLErrors.

A document selecting several fields should need every scope those fields require.

Acceptance criteria

  • Each query, mutation and subscription field declares the scope it needs, matching its REST equivalent in the table above
  • A key lacking the scope is refused with the same code REST uses, not a generic GraphQL error
  • A multi-field document requires the union of its fields' scopes
  • A test per field asserting refusal with an insufficient key and success with a sufficient one
  • A test that fails if a new resolver is added with no scope declared — the registry and the schema must not drift
  • docs/graphql.md documents the scope each field needs

Notes

hasScope is in src/api/auth.ts; do not reimplement it. src/__tests__/graphqlScreener.test.ts and src/__tests__/graphqlPairs.test.ts show how to drive the GraphQL layer in tests.

The last criterion is the one that matters most — without it this comes back the next time someone adds a resolver.

Activity

  1. sparkysolutions10-pixel commented on Oct 24, 2026

    @sparkysolutions10-pixel
    Contributor

    Hi @Miracle656, I would like to work on this issue. Please assign it to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions