Implementing proper authentication and authorization in GraphQL requires careful consideration at both server and client levels.
Authentication Patterns
JWT Authentication
Server setup:
javascript
import { ApolloServer } from '@apollo/server';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET;
const context = async ({ req }) => {
const token = req.headers.authorization || '';
let user = null;
if (token) {
try {
user = jwt.verify(token.replace('Bearer ', ''), JWT_SECRET);
} catch (err) {
console.error('Invalid token');
}
}
return { user };
};
const server = new ApolloServer({
typeDefs,
resolvers,
context
});Authentication Middleware
javascript
const authMiddleware = (resolver) => {
return (parent, args, context, info) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
return resolver(parent, args, context, info);
};
};
const resolvers = {
Query: {
me: authMiddleware((parent, args, context) => {
return context.user;
})
}
};Authorization
Role-Based Access Control
javascript
const hasRole = (role) => (resolver) => {
return (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
if (!context.user.roles.includes(role)) {
throw new ForbiddenError('Insufficient permissions');
}
return resolver(parent, args, context);
};
};
const resolvers = {
Mutation: {
deleteUser: hasRole('admin')((parent, { id }, context) => {
return database.deleteUser(id);
})
}
};Custom Directives
graphql
directive @auth on FIELD_DEFINITION
directive @admin on FIELD_DEFINITION
directive @hasRole(role: String!) on FIELD_DEFINITION
type Query {
me: User! @auth
users: [User!]! @admin
settings: Settings! @hasRole(role: "admin")
}Directive Implementation
javascript
import { defaultFieldResolver } from 'graphql';
const authDirective = (schema) => {
const typeMap = schema.getTypeMap();
Object.values(typeMap).forEach(type => {
if (type.getFields) {
const fields = type.getFields();
Object.values(fields).forEach(field => {
const directives = field.astNode.directives;
const authDirective = directives?.find(d => d.name.value === 'auth');
if (authDirective) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async (parent, args, context, info) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
return resolve(parent, args, context, info);
};
}
});
}
});
return schema;
};Context Building
Building Context with User
javascript
const context = async ({ req, res }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
user = await getUserFromToken(token);
}
return {
user,
req,
res,
dataSources: {
userAPI: new UserAPI({ user }),
productAPI: new ProductAPI()
}
};
};DataLoader with Auth
javascript
const context = async ({ req }) => {
const user = await authenticateUser(req);
return {
user,
loaders: {
user: new DataLoader(async (ids) => {
return await db.users.find({
where: {
id: { in: ids },
// Only return users user has access to
organizationId: user.organizationId
}
});
})
}
};
};Client-Side Auth
Apollo Auth Link
javascript
import { ApolloLink, setContext } from '@apollo/client';
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});Token Refresh
javascript
import { onError } from '@apollo/client/link/error';
import { fromPromise } from '@apollo/client';
let isRefreshing = false;
let pendingRequests = [];
const refreshToken = () => {
if (!isRefreshing) {
isRefreshing = true;
return fromPromise(
refreshAccessToken()
.then(token => {
pendingRequests.forEach(cb => cb(token));
pendingRequests = [];
return token;
})
.finally(() => {
isRefreshing = false;
})
);
}
return fromPromise(
new Promise(resolve => {
pendingRequests.push(resolve);
})
);
};
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors) {
const isAuthError = graphQLErrors.some(
err => err.extensions.code === 'UNAUTHENTICATED'
);
if (isAuthError) {
return fromPromise(
refreshToken()
).flatMap(token => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: `Bearer ${token}`
}
}));
return forward(operation);
});
}
}
});Protected Queries
Client-Side Guards
javascript
import { useQuery } from '@apollo/client';
import { useAuth } from './auth-context';
function ProtectedComponent() {
const { user, loading } = useAuth();
const { data, loading: queryLoading } = useQuery(GET_SENSITIVE_DATA, {
skip: !user
});
if (loading || queryLoading) return <Loading />;
if (!user) return <LoginPrompt />;
return <SensitiveData data={data} />;
}Higher-Order Component
javascript
import { useAuth } from './auth-context';
function withAuth(Component) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <Loading />;
if (!user) return <Navigate to="/login" />;
return <Component {...props} user={user} />;
};
}
const Dashboard = withAuth(({ user }) => {
return <div>Welcome, {user.name}</div>;
});Row-Level Security
User-Specific Data
javascript
const resolvers = {
Query: {
posts: (parent, args, context) => {
return context.dataSources.postAPI.getPostsForUser(
context.user.id
);
}
}
};Organization Filtering
javascript
class PostAPI extends RESTDataSource {
async getPosts() {
const response = await this.get('posts', {
headers: {
'X-User-ID': this.context.user.id,
'X-Org-ID': this.context.user.organizationId
}
});
return response.data;
}
}Security Best Practices
Input Validation
javascript
const resolvers = {
Mutation: {
updateUser: async (parent, { id, input }, context) => {
// Validate user owns this account
if (id !== context.user.id) {
throw new ForbiddenError('Can only update own profile');
}
// Sanitize input
const sanitized = sanitizeUserInput(input);
return await database.updateUser(id, sanitized);
}
}
};Rate Limiting
javascript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/graphql', limiter);Query Complexity Analysis
javascript
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const complexityLimitRule = createComplexityLimitRule(1000);
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimitRule]
});Conclusion
Proper authentication and authorization are critical for GraphQL APIs. Implement defense in depth with multiple layers of security.