Skip to main content

Security

Brightspot GraphQL endpoints ship with layered security controls: authentication via API access options and keys, CORS for browser clients, introspection gating, and three complementary defenses against expensive queries—timeouts, depth limits, and complexity analysis. All of them apply uniformly to GCA, code generator, and framework endpoints.

Authentication and API access

Every endpoint declares an access option via getApiAccessOption() (or the API Access field on editorial endpoints):

OptionBehavior
GraphQLApiAccessOptionExplicit (default)Requests must present a valid API key associated with a client.
GraphQLApiAccessOptionImplicitUnauthenticated access allowed; clients may still identify themselves for attribution.

API clients and keys are managed in the CMS under Admin → APIs. Requests authenticate with either:

  • the X-API-Key header (or apiKey query parameter), or
  • the X-Client-ID / X-Client-Secret header pair.

Issue a separate client per consumer: keys become individually revocable, and analytics attribute traffic per client. For fully custom authentication and authorization, implement your own GraphQLApiAccessOption—it decides per request what client (if any) the caller is and whether one is required.

Authorization granularity

Endpoint-level access decides who can call the API. What they can see is governed by the schema you expose—per-endpoint entry types, field filters, and reference filters. The practical pattern for differing permission levels is multiple endpoints with different schema settings rather than per-field runtime authorization. See GCA Best Practices.

CORS

Browser-based clients need CORS headers. Configure allowed origins, allowed headers, max age, and credentials support by overriding updateCorsConfiguration(GraphQLCorsConfiguration) in code, or through the CORS Configuration field on editorial endpoints. Endpoint-specific headers (like the site-selection header used by GCA site suppliers) are added automatically.

Introspection controls

GraphQL introspection (__schema queries) reveals your entire schema. The default introspectionQueryRule allows it everywhere except production. Keep that default for public endpoints; loosen it only for internal tooling endpoints that need live schema discovery. (The GCA's separate type-system introspection is likewise opt-in.)

Query timeouts

Set a hard execution ceiling with timeout(Duration) in schema settings (or Query Timeout editorially). When exceeded, the operation aborts with a timeout error whose extensions include the configured timeout and elapsed time. GCA endpoints additionally support per-query timeouts via options: {timeout: ...}, capped by maximumQueryTimeout(...).

Maximum query depth

maximumQueryDepth(Integer) rejects queries nested deeper than the limit before execution—a cheap defense against pathological recursive queries over self-referencing content graphs. Introspection queries are exempt so tooling keeps working where introspection is allowed.

Query complexity

Depth alone doesn't measure cost—a shallow query can fan out enormously. Complexity analysis assigns every field a point cost and enforces budgets:

  • Field costs. Every field defaults to defaultFieldComplexity (1 point); fields with different costs carry the @complexity(value: ...) directive in the SDL, so consumers can see costs. The GCA charges referenceFieldComplexity for reference traversals (database fetches) and embeddedFieldComplexity for embedded data, and __typename/introspection fields are free.
  • Static budget — evaluated before execution from the query shape. Queries over budget are rejected outright.
  • Dynamic budget — consumed during execution as data actually flows (a list of 100 items multiplies its children's costs). Execution stops when the budget runs dry.

Configure budgets by adding QueryComplexityManagers (static and/or dynamic) in schema settings or the editorial Security cluster. A manager simply implements consume(amount) → remaining, so budgets can be per-request, per-client, or backed by shared quota systems—this is also the building block for rate limiting by cost.

Responses include a queryComplexity extension reporting consumed/remaining for each budget, so consumers can observe their usage:

1
"extensions": {
2
"queryComplexity": {
3
"static": {"consumed": 42, "remaining": 958},
4
"dynamic": {"consumed": 311, "remaining": 689}
5
}
6
}

Locking down query expressiveness

Beyond budgets, the GCA can structurally remove expensive or sensitive capabilities per endpoint:

  • onlyAllowUniqueIndexLookups() — no arbitrary queries at all, only single-object fetches.
  • Persisted-query allowlisting — accept only statically registered queries, eliminating arbitrary query execution entirely.
  • disallowAllStateAccess() — views only; no record state, no mutations.

Error verbosity

Execution errors are terse by default. The GCA's verboseClientErrors() adds detail useful in development but potentially sensitive in production—leave it off on public endpoints and rely on server logs instead.

Production checklist

  • Explicit API access with per-consumer clients and keys
  • CORS restricted to known origins
  • Introspection disabled in production (default)
  • timeout, maximumQueryDepth, and complexity budgets configured
  • Static persisted queries (allowlist mode) for high-exposure public endpoints
  • Verbose client errors off
  • Mutations only on endpoints whose audience writes, with site/user suppliers wired

Next steps

Was this page helpful?

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.