A graphql query unauthorised error means your API rejected the request because it could not verify who is making it, or confirmed the user does not have the required permissions. In practice you will see one of two distinct responses: an HTTP 401 with the GraphQL error code UNAUTHENTICATED (the token is missing, expired, or malformed) or an HTTP 403 / error code FORBIDDEN (the token is valid but the user lacks access to that specific field or resolver). Knowing which one you are dealing with is the fastest way to fix it.
Key Takeaways
- Fix fast: Check the
extensions.codefield in the error response first —UNAUTHENTICATEDvsFORBIDDENtells you exactly where to look. - Authenticate once, authorise everywhere: Passing the token at the gateway is not enough — every resolver that touches protected data must verify permissions independently.
- Use middleware: Centralising authorisation logic in middleware or
graphql-shieldremoves duplication and makes your policy auditable in one place. - Disable introspection in production: Leaving it on gives attackers a free map of every field and relationship in your schema.
- Pair auth with rate limiting: Even a correct token should not be able to hammer your API with expensive nested queries without consequence.
What the error actually looks like
Before diving into solutions, it helps to see exactly what GraphQL returns when authorisation fails. Unlike REST, GraphQL almost always returns HTTP 200 even for auth errors — the real status lives inside the JSON body:
// UNAUTHENTICATED — token is missing or invalid
{
"errors": [
{
"message": "Not authenticated",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "UNAUTHENTICATED",
"http": { "status": 401 }
}
}
],
"data": { "user": null }
}
// FORBIDDEN — token is valid but user lacks permission
{
"errors": [
{
"message": "Not authorised to access field 'paymentMethod'",
"extensions": {
"code": "FORBIDDEN",
"http": { "status": 403 }
}
}
]
}If you see UNAUTHENTICATED, the fix is in how you pass the token. If you see FORBIDDEN, the fix is in your resolver-level permission logic.
Check how your server maps these codes to HTTP responses in the GraphQL HTTP status codes guide — misconfigured status mappings are a common source of confusing error behaviour.
Introduction
GraphQL has transformed API development with its flexible query language, but that flexibility shifts security responsibility from infrastructure-level endpoint controls to application-level resolver controls. Unauthorised GraphQL queries are one of the most exploited vulnerabilities in modern APIs: the single-endpoint design means a single misconfigured resolver can expose data across your entire object graph. Understanding how authorisation failures happen — and how to prevent them at every layer — is essential for any team running GraphQL in production.
GraphQL authorisation vulnerabilities explained
GraphQL’s rapid enterprise adoption has created complex authorisation challenges that many teams underestimate. Unlike REST APIs where a route guard at /admin/users protects the entire resource, GraphQL’s single endpoint requires field-level and resolver-level authorisation checks throughout the entire query execution chain.
“Broken authentication and authorization provides opportunities for attackers targeting GraphQL. Two of the main attack vectors include: Authentication bypass — where attackers exploit weak authentication mechanisms. Exploiting authorization flaws — taking advantage of insufficient access controls to get to unauthorized data.”
— Tyk.io, 2024
Source link
The OWASP API Security Top 10 consistently lists broken object-level authorisation (BOLA) as a leading category, and GraphQL is especially susceptible because of its nested relationship traversal. Many developers assume verifying identity once at the request level is sufficient — it is not. Each resolver in the execution chain can reach different data, and each needs its own permission check.
| Aspect | REST APIs | GraphQL APIs |
|---|---|---|
| Endpoints | Multiple endpoints | Single endpoint |
| Authorisation scope | Per-endpoint controls | Per-field / per-resolver controls |
| Query flexibility | Fixed responses | Dynamic nested queries |
| Security complexity | Lower | Higher |
The unique security challenges of GraphQL
GraphQL’s nested query capability lets clients traverse complex object relationships in a single request. That is powerful for developers — and equally powerful for attackers. A user might have permission to read a Customer object but not the paymentMethods or internalNotes fields nested inside it. If your resolver for those fields does not independently verify permissions, the data is exposed regardless of top-level checks.
Query depth and complexity introduce additional attack vectors: deeply nested queries can bypass shallow authorisation checks, extract unauthorised data through relationship traversal, and exhaust server resources before any rate limit fires. The resolver chain model means that a failure at any depth can leak sensitive information through error messages or partial responses.
Authorisation failures can surface as GraphQL validation errors when the schema hides fields from unauthorised users — learn to tell the two apart before debugging.
Business impacts of GraphQL authorisation flaws
A graphql query unauthorised bypass is never just a technical problem. The business consequences are immediate and lasting:
- Average data breach cost: $4.88 million globally (IBM Cost of a Data Breach Report 2024)
- GDPR fines up to 4% of annual global revenue
- Customer trust recovery: 12–24 months on average
- Regulatory investigation duration: 6–18 months
- Mandatory breach notification within 72 hours under GDPR
Compliance frameworks like GDPR, HIPAA, and PCI DSS impose specific access-control requirements that GraphQL authorisation failures can directly violate. Beyond fines, unauthorised query access often reveals your database structure and relationship patterns — giving attackers a map for deeper, more targeted attacks on related systems.
Anatomy of an unauthorised GraphQL query
An unauthorised query succeeds when the authorisation logic fails to validate permissions at one or more critical points during execution. The three most common failure points are:
- Missing token in request headers — the
Authorizationheader is absent or uses the wrong format (Bearer <token>vsToken <token>). - Token validation not wired into context — the token exists but the middleware does not parse it into
context.user, so resolvers see an empty context. - No resolver-level check — even with a valid, correctly parsed token, the resolver executes without ever inspecting
context.user.rolesor permissions.
Here is a minimal example of how each layer should connect in a Node.js / Apollo Server setup:
// 1. Middleware: parse JWT and attach user to context
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return { user: null };
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
return { user };
} catch {
return { user: null }; // expired or malformed
}
},
});
// 2. Resolver: check context before returning data
const resolvers = {
Query: {
adminUsers: (_, __, context) => {
if (!context.user) {
throw new GraphQLError("Not authenticated", {
extensions: { code: "UNAUTHENTICATED", http: { status: 401 } },
});
}
if (!context.user.roles.includes("ADMIN")) {
throw new GraphQLError("Not authorised", {
extensions: { code: "FORBIDDEN", http: { status: 403 } },
});
}
return UserService.getAllUsers();
},
},
};The OWASP framework specifically calls out Broken Object Level Authorization (BOLA) as a primary API risk. GraphQL is particularly susceptible because object-relationship traversal can reach unauthorised data through multiple indirect paths — none of which a single gateway check will catch.
When testing auth flows manually, use GraphQL Playground with an authorization header to simulate both authenticated and unauthenticated scenarios without writing client code.
Common authorisation implementation flaws
The most frequent mistakes teams make — and how to fix them:
| Flawed approach | Secure alternative | Risk level |
|---|---|---|
| No resolver-level checks | Authorise inside every resolver that touches protected data | Critical |
| Client-side validation only | Always validate on the server — client checks are cosmetic | High |
| Generic error messages leaking field names | Return consistent, non-revealing errors (FORBIDDEN) | Medium |
| Single auth check at gateway | Continuous authorisation verification per resolver | High |
| Introspection enabled in production | Disable introspection; restrict schema discovery | High |
Field-level permissions are the most commonly skipped layer: developers secure object access but forget to restrict individual sensitive fields within an authorised object — SSNs, internal notes, payment data. Schema directives help here but must be applied consistently; a single unguarded field in a related resolver undoes the rest.
The problem with default GraphQL behaviours
Most GraphQL libraries ship with defaults optimised for development convenience, not production security:
- Introspection enabled — gives any client a full map of your schema, types, and relationships.
- Detailed error messages — reveal database structure, field names, and internal logic in stack traces.
- No query depth or complexity limits — allows resource-exhaustion attacks through deeply nested queries.
- Permissive CORS — allows cross-origin requests from any domain.
Audit all four of these before going to production. Disabling introspection alone removes the easiest reconnaissance step from any attacker’s playbook.
How attackers exploit authorisation gaps
Security researchers have documented a consistent attack progression across GraphQL implementations in different industries:
- Discover the GraphQL endpoint (often
/graphqlor/api/graphql) - Execute an introspection query to map the full schema
- Identify sensitive fields and object relationships
- Craft queries that traverse relationships to reach unauthorised data
- Exploit partial authorisation checks to extract data through indirect paths
- Escalate access using discovered relationships and business logic
Data exfiltration via relationship traversal is the most common technique: an attacker starts with a legitimately accessible object (say, their own Order) and traverses nested relationships to reach other customers’ data, payment methods, or admin records — exploiting gaps in resolver-level authorisation at each level.
Impact of successful exploits
- Unauthorised access to customer PII and financial records
- Privilege escalation to admin functions through relationship traversal
- Regulatory compliance violations (GDPR, HIPAA, PCI DSS)
- Brand reputation damage and customer churn
- Legal liability, litigation costs, and mandatory breach notifications
- Operational disruption during incident response and remediation
Real-world breach patterns
A recurring pattern in reported GraphQL breaches: authenticated customers could access other customers’ order histories and payment methods through crafted queries that bypassed field-level authorisation checks on nested resolvers. The top-level Order resolver was secured; the nested paymentMethod and shippingAddress resolvers were not.
In healthcare GraphQL APIs, authorisation failures on nested medical-record relationships allowed users to traverse from their own patient record to unrelated providers’ records — exposing data across patients who shared a care pathway. These incidents consistently share one root cause: missing resolver-level checks on nested objects.
Detecting and testing for authorisation vulnerabilities
Testing GraphQL authorisation requires a different approach from REST. The single endpoint means you cannot rely on URL-based access controls — you must probe field-level and relationship-level permissions explicitly.
- Run introspection to map the full schema and all available fields
- Identify sensitive fields: PII, financial data, admin-only operations
- Test each sensitive field with: no token, expired token, valid token with wrong role
- Craft nested queries that traverse from authorised objects to related sensitive data
- Validate error responses do not leak field names or internal details
- Verify rate limiting fires correctly on repeated failed auth attempts
Use GraphQL Playground variables to parametrise your test queries and systematically probe authorisation boundaries without rewriting queries each time.
Tools for security testing
| Tool | Type | Key feature | Best for |
|---|---|---|---|
| GraphQL Voyager | Open source | Interactive schema visualisation | Mapping relationship attack surface |
| InQL (Burp plugin) | Open source | Introspection, fuzzing, query generation | Penetration testing |
| GraphQL Cop | Open source | Automated security scanning | CI/CD security gates |
| Burp Suite Pro | Commercial | Full manual testing suite | Comprehensive assessments |
Automated tools are useful for schema analysis and known vulnerability patterns, but manual testing remains essential for business-logic authorisation flaws — for example, verifying that a user cannot access another user’s resources by changing an ID in a query.
Analysing your schema for security gaps
Schema analysis is the fastest way to prioritise where to focus authorisation testing. Look for:
- Fields with sensitive names (
ssn,password,internalNote,paymentMethod) that lack directives or resolver-level checks - Relationship fields that connect user-owned objects to objects owned by others
- Mutations that modify data without explicit role or ownership validation
- Queries that accept ID arguments — a classic BOLA vector
Implementing robust authorisation in GraphQL
Secure GraphQL authorisation requires defence-in-depth: validating permissions at the middleware layer, within individual resolvers, and optionally at the schema level via directives. The most maintainable approach centralises your authorisation rules so they are auditable in one place.
“Authentication is a cornerstone of securing GraphQL APIs, ensuring that only trusted clients and users can access or modify data. Because GraphQL exposes a single, flexible endpoint, strong authentication strategies, combined with fine-grained authorization, are essential to protect sensitive operations.”
— BrowserStack, 2024
Source link
Authentication establishes who the user is; authorisation determines what that user can do at each step of query execution. The two are distinct, and conflating them is the most common source of FORBIDDEN errors that appear only on specific nested fields after a user is already authenticated.
Best practices by authentication method
- JWT: Validate signature, expiration (
exp), issuer (iss), and audience (aud) on every request. Do not trust claims without signature verification. - OAuth 2.0: Validate scopes at the resolver level, not just at the gateway. Handle token refresh gracefully and reject expired tokens with
UNAUTHENTICATED. - API Keys: Rotate regularly, enforce per-key rate limits, and never log the full key value in request logs.
- Session-based: Validate session on each request, enforce idle timeout, and guard against race conditions in concurrent resolver execution.
Manage environment-specific auth configurations (dev, staging, production) with Spring profiles active to isolate security rules without code duplication.
Using middleware and graphql-shield for authorisation
Middleware intercepts every request before resolvers execute, making it the right place to parse tokens, build the user context, and reject unauthenticated requests early — before they consume resources. But middleware alone cannot enforce field-level permissions. That is where graphql-shield comes in:
import { shield, rule, and } from "graphql-shield";
const isAuthenticated = rule({ cache: "contextual" })(
async (parent, args, ctx) => ctx.user !== null || new Error("Not authenticated")
);
const isAdmin = rule({ cache: "contextual" })(
async (parent, args, ctx) =>
ctx.user?.roles.includes("ADMIN") || new Error("Admin access required")
);
export const permissions = shield({
Query: {
allUsers: and(isAuthenticated, isAdmin),
myProfile: isAuthenticated,
},
Mutation: {
deleteUser: and(isAuthenticated, isAdmin),
},
User: {
paymentMethods: isAuthenticated, // field-level rule
internalNotes: isAdmin, // admin-only field
},
});Benefits of this pattern:
- All authorisation rules in a single auditable file
- Early request termination on auth failure improves performance
- Field-level rules prevent relationship-traversal data leaks
- Easier compliance auditing — security policy is readable by non-engineers
When your middleware returns auth errors, make sure they are formatted correctly using ResponseEntity patterns so clients receive properly structured 401/403 responses.
Advanced authorisation patterns
As your application grows, simple role checks give way to more sophisticated access control models. Choose based on your complexity and performance requirements:
| Pattern | Complexity | Performance | Best use case |
|---|---|---|---|
| Field-level authorisation | High | Medium | Granular per-field access control |
Directive-based (@auth) | Medium | High | Schema-driven, declarative security |
| Role-based (RBAC) | Low | High | Simple role hierarchies |
| Attribute-based (ABAC) | High | Lower | Complex contextual policies |
Directive-based access control is one of the cleanest options for teams who want security rules visible directly in the schema:
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role { ADMIN USER GUEST }
type Query {
allUsers: [User] @auth(requires: ADMIN)
myProfile: User @auth(requires: USER)
publicInfo: Info # no directive = public
}
type User {
id: ID!
email: String @auth(requires: USER)
internalNotes: String @auth(requires: ADMIN)
}Reducing attack surface with non-variable queries
Query designs that rely on server-side context (the authenticated user’s identity from the token) rather than client-supplied parameters dramatically reduce the BOLA attack surface. Instead of query { user(id: "123") { ... } } — where a user can manipulate the ID — prefer query { me { ... } }, where the server resolves the identity from context.user.id. This eliminates the most common direct object reference vulnerability in GraphQL.
Continuous authorisation and monitoring
Implementing authorisation once is not enough. Credentials are revoked, roles change, and new fields are added to the schema. Continuous authorisation means your permission checks re-validate context on every request and your monitoring surfaces anomalies before they become breaches.
Runtime monitoring for GraphQL should track: repeated UNAUTHENTICATED errors from a single IP (credential stuffing), unusually deep or complex queries (reconnaissance or DoS), and successful queries that access sensitive fields from new geographic locations or unusual times.
For production observability, integrate your auth monitoring with a GraphQL monitoring setup to correlate authorisation failures with query performance and error rates in real time.
Rate limiting and abuse prevention
Standard request-count rate limits do not adequately protect GraphQL: a single deeply nested query can consume 100× more resources than a simple one. Use query complexity scoring alongside request-count limits:
| Strategy | How it works | Best for | Tradeoff |
|---|---|---|---|
| Query depth limiting | Count nesting levels, reject above threshold | Preventing traversal attacks | May block legitimate deep queries |
| Query complexity scoring | Assign cost to each field, reject above budget | Resource-aware rate limiting | Requires tuning cost weights |
| Rate limiting by user identity | Track query count / complexity per token | Per-user abuse prevention | Requires authenticated requests |
| Persisted / whitelisted queries | Only allow pre-approved query hashes | Maximum security for closed APIs | Reduces GraphQL flexibility |
Critically, rate limiting must integrate with your authorisation layer: unauthenticated requests that repeatedly trigger UNAUTHENTICATED errors should be throttled aggressively to prevent credential brute-forcing.
See the practical implementation guide for GraphQL rate limiting and pair it with GraphQL timeout configuration to protect against slow, resource-exhausting query attacks.
More GraphQL Security & API Guides
- GraphQL HTTP Status Codes — understand which status codes your API should return for auth errors, validation failures, and server errors.
- GraphQL Validation Errors — differentiate between schema validation failures and authorisation errors to debug faster.
- Playground Authorization Header — set up auth headers in GraphQL Playground to test protected endpoints during development.
- GraphQL Rate Limiting — protect your API from abuse with request-count and complexity-based rate limiting strategies.
- GraphQL Monitoring — detect suspicious query patterns and authorisation anomalies in production.
- GraphQL Mutation Return Nothing — secure mutations that don’t return data without leaking authorisation state through empty responses.
Frequently Asked Questions
A GraphQL query unauthorised error occurs when the server cannot verify the requester’s identity (UNAUTHENTICATED) or confirms the user lacks permission for a specific field or operation (FORBIDDEN). The most common causes are: a missing or malformed Authorization: Bearer <token> header, an expired JWT, a token that is not being parsed into the GraphQL context, or a resolver that never checks permissions before returning data. Check the extensions.code field in the error JSON to distinguish between the two cases before debugging.
Authentication answers “who are you?” — it verifies identity through a token, credential, or session. Authorisation answers “what can you do?” — it determines which fields, objects, and mutations an authenticated user is permitted to access. In GraphQL, authentication typically happens once at the middleware layer (JWT validation, session lookup), while authorisation must happen continuously at each resolver that touches protected data. Getting only authentication right and skipping resolver-level authorisation is the root cause of most GraphQL unauthorised errors.
The most maintainable approach combines middleware with a rule library like graphql-shield. Use middleware to parse the token and attach the user to context. Use graphql-shield to define per-query, per-mutation, and per-field rules in a single file. For each protected resolver, check context.user and context.user.roles before executing. Throw GraphQLError with extensions.code: "UNAUTHENTICATED" (HTTP 401) for missing/invalid tokens and "FORBIDDEN" (HTTP 403) for insufficient permissions. Test all protected fields individually — not just top-level queries.
Send the token in the HTTP Authorization header using the Bearer scheme: Authorization: Bearer eyJhbGci.... In GraphQL Playground, add it under the “HTTP Headers” tab as {"Authorization": "Bearer <your_token>"}. In Apollo Client, configure the authLink to inject the header on every request. Common mistakes: using Token instead of Bearer, forgetting to include the space between the scheme and the token value, or sending the token in the request body instead of the header.
Middleware intercepts every incoming request before resolvers run. It parses and validates the authentication token, builds a user context object with identity and roles, and can terminate the request immediately for completely unauthenticated calls — before any query processing consumes resources. However, middleware alone handles authentication, not field-level authorisation. Pair it with resolver-level checks or a library like graphql-shield to enforce which authenticated users can access which specific fields and operations.
GraphQL authorisation flaws can expose customer PII, financial records, and business data through relationship-traversal attacks — often at a larger scale than equivalent REST API breaches because a single query can traverse multiple object relationships. Consequences include GDPR/HIPAA regulatory fines, mandatory breach notifications within 72 hours, class action litigation, and 12–24 months of customer trust recovery. The IBM 2024 Cost of a Data Breach Report puts the average breach cost at $4.88 million globally — a figure that makes early investment in authorisation testing a clear ROI decision.




