GraphQL Authentication â Securing Your API with JWT and Context
In this tutorial, you will learn about Graphql Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL authentication is typically handled in the context layer, where tokens are verified once per request and the authenticated user is made available to all resolvers.
What You'll Learn
You will learn how to implement JWT authentication in Apollo Server, create auth context, build directive-based auth guards, handle token refresh, and integrate with third-party auth providers.
Why Authentication Matters
GraphQL APIs need authentication to protect user data and enforce access control. Without authentication, anyone can query any data. DodaTech's Durga Antivirus Pro uses JWT-based authentication â users log in via the web app, receive a signed JWT, and include it in every GraphQL request. The server verifies the token in the context function and attaches the user to every resolver.
sequenceDiagram
participant Client
participant AuthAPI as Auth Endpoint
participant GraphQL
participant Context
Client->>AuthAPI: POST /login (email, password)
AuthAPI-->>Client: JWT token
Client->>GraphQL: query { devices } + Authorization header
GraphQL->>Context: Verify token
Context->>Context: Decode JWT, find user
Context-->>GraphQL: { user: User, db, loaders }
GraphQL->>Resolver: context.user available
Resolver-->>Client: Filtered response
Prerequisites: GraphQL server setup. Understanding of JWT and HTTP headers.
JWT Authentication in Context
const jwt = require('jsonwebtoken');
const { ApolloServer, AuthenticationError } = require('apollo-server');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
// Get token from Authorization header
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: null;
let user = null;
if (token) {
try {
// Verify and decode the JWT
const decoded = jwt.verify(token, JWT_SECRET);
// Fetch user from database (ensures user still exists)
user = await db.users.findById(decoded.userId);
} catch (err) {
// Token invalid or expired â user stays null
// Do NOT throw here â let individual resolvers decide
console.warn('Invalid token:', err.message);
}
}
return {
user,
db,
loaders: createLoaders(),
};
},
});
Resolver-Level Auth Checks
const { AuthenticationError, ForbiddenError } = require('apollo-server');
const resolvers = {
Query: {
myDevices: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
return context.db.devices.findByUserId(context.user.id);
},
allDevices: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
if (context.user.role !== 'ADMIN') {
throw new ForbiddenError('Only admins can list all devices');
}
return context.db.devices.findAll();
},
},
Device: {
// Owner-only fields
internalNotes: (parent, args, context) => {
if (!context.user || parent.userId !== context.user.id) {
return null; // Hide sensitive data from unauthorized users
}
return parent.internalNotes;
},
},
};
Directive-Based Auth Guards
enum Role {
ADMIN
ANALYST
VIEWER
}
directive @auth(requires: Role!) on FIELD_DEFINITION | OBJECT
type Threat {
id: ID!
name: String!
severity: Severity!
# Only admins can see internal notes
internalNotes: String @auth(requires: ADMIN)
# Analysts and admins can see analysis
analysis: String @auth(requires: ANALYST)
}
type Query {
threats: [Threat!]!
sensitiveThreats: [Threat!]! @auth(requires: ANALYST)
}
const { mapSchema, getDirectives, MapperKind } = require('@graphql-tools/utils');
function authDirectiveTransformer(schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirectives(schema, fieldConfig)?.auth;
if (!authDirective) return;
const { requires } = authDirective;
const originalResolver = fieldConfig.resolve || defaultFieldResolver;
fieldConfig.resolve = (source, args, context, info) => {
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
const roleHierarchy = { ADMIN: 3, ANALYST: 2, VIEWER: 1 };
if ((roleHierarchy[context.user.role] || 0) < (roleHierarchy[requires] || 0)) {
throw new ForbiddenError(`Requires ${requires} role`);
}
return originalResolver(source, args, context, info);
};
},
});
}
const server = new ApolloServer({
typeDefas,
resolvers,
schemaTransforms: [authDirectiveTransformer],
});
Token Refresh Pattern
// Refresh token mutation
const resolvers = {
Mutation: {
refreshToken: async (_, { refreshToken }, context) => {
try {
const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
const user = await db.users.findById(decoded.userId);
if (!user) {
throw new AuthenticationError('User not found');
}
// Issue new access token
const newToken = jwt.sign(
{ userId: user.id, role: user.role },
JWT_SECRET,
{ expiresIn: '15m' }
);
return {
token: newToken,
user,
};
} catch (err) {
throw new AuthenticationError('Invalid refresh token');
}
},
},
};
Integrating with Auth0
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' });
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(null, key?.publicKey || key?.rsaPublicKey);
});
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return { user: null };
return new Promise((resolve) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
audience: 'https://api.dodatech.com',
issuer: 'https://your-domain.auth0.com/',
}, (err, decoded) => {
if (err) return resolve({ user: null });
resolve({ user: decoded });
});
});
},
});
Common Mistakes
1. Throwing Auth Errors in Context
If context throws, the entire request fails â even for public queries. Let context return { user: null } and let individual resolvers enforce auth.
2. Not Validating Token on Every Request
Caching JWT verification results across requests introduces security holes. Verify the token on every request in the context function.
3. Storing Sensitive Data in JWT
JWTs are signed, not encrypted. Never store passwords, API keys, or sensitive PII in the token payload.
4. Using Short-Lived Tokens Without Refresh
15-minute access tokens without refresh tokens force users to log in every 15 minutes. Implement a refresh token flow.
5. Not Checking Revoked Tokens
If a user logs out, their JWT is still valid until expiry. Maintain a token blacklist (Redis) or check user status in the context function.
Practice Questions
- Where does authentication happen in GraphQL?
- How do you access the authenticated user in resolvers?
- What is the recommended pattern for public vs private fields?
- How do you implement role-based access control?
- Why should context not throw auth errors?
Answers:
- Authentication happens in the context function â verify the token from request headers once per request and attach the user to context.
- Access
context.userin any resolver. It's set by the context function and available to all resolvers in that request. - Let context attach
userornull. Resolvers checkcontext.userto decide visibility. Directive-based guards provide declarative control on fields. - Define role levels (ADMIN, ANALYST, VIEWER) in an enum. Implement role checks in resolvers or via an
@auth(requires: Role)directive transformer. - If context throws auth errors, the entire request fails â even public queries. Let context attach
nulland let individual resolvers decide.
Challenge: Design a complete authentication system for DodaTech's GraphQL API. Include JWT-based login with access and refresh tokens, context-based auth with user loading, directive-based role guards (ADMIN, ANALYST, VIEWER), token refresh mutation, and integration with Auth0 for SSO.
FAQ
Mini Project
Build a complete authentication system for DodaTech's GraphQL API. Implement JWT authentication with access tokens (15min) and refresh tokens (7 days), a @auth directive for role-based access control, a login mutation that returns tokens, a refreshToken mutation, a logout mutation with token blacklisting, and integrate with Auth0 for enterprise SSO.
What's Next
| Topic | Description |
|---|---|
| Authorization | Role-based access control patterns |
| Error Handling | Error patterns in GraphQL |
| Security | Depth limiting, cost analysis |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro