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):
| Option | Behavior |
|---|---|
GraphQLApiAccessOptionExplicit (default) | Requests must present a valid API key associated with a client. |
GraphQLApiAccessOptionImplicit | Unauthenticated 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-Keyheader (orapiKeyquery parameter), or - the
X-Client-ID/X-Client-Secretheader 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.
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 chargesreferenceFieldComplexityfor reference traversals (database fetches) andembeddedFieldComplexityfor 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
- Persisted Queries — allowlisting and performance
- Analytics — monitoring per-client usage
- GCA Best Practices — schema-level security guidance