GraphQL
Authentication and Authorization
Securing Your GraphQL API
Authentication (verifying who you are) and authorization (verifying what you can do) are critical for any production GraphQL API. In this lesson, we'll implement secure authentication using JWT tokens and role-based access control.
Authentication vs Authorization
- Authentication: Verifying the identity of a user (login)
- Authorization: Verifying what actions a user is allowed to perform (permissions)
Setting Up JWT Authentication
Install required packages:
npm install jsonwebtoken bcryptjs
npm install --save-dev @types/jsonwebtoken @types/bcryptjs
User Registration and Login
First, create resolvers for user registration and login:
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const typeDefs = `
type User {
id: Int!
email: String!
name: String!
role: String!
}
type AuthPayload {
token: String!
user: User!
}
type Mutation {
register(email: String!, password: String!, name: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
}
`;
const resolvers = {
Mutation: {
register: async (parent, { email, password, name }, { prisma }) => {
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser) {
throw new Error('User already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
role: 'USER'
}
});
// Generate JWT token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, user };
},
login: async (parent, { email, password }, { prisma }) => {
// Find user
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) {
throw new Error('Invalid credentials');
}
// Verify password
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
throw new Error('Invalid credentials');
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, user };
}
}
};
Important: Set a strong
JWT_SECRET in your .env file. This secret is used to sign and verify JWT tokens. Never commit it to version control.
Context-Based Authentication
Extract the user from the JWT token in the context:
const jwt = require('jsonwebtoken');
const getUserFromToken = async (token, prisma) => {
if (!token) return null;
try {
// Remove "Bearer " prefix if present
const cleanToken = token.replace('Bearer ', '');
// Verify and decode token
const decoded = jwt.verify(cleanToken, process.env.JWT_SECRET);
// Fetch user from database
const user = await prisma.user.findUnique({
where: { id: decoded.userId }
});
return user;
} catch (error) {
console.error('Token verification failed:', error.message);
return null;
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers.authorization || '';
const user = await getUserFromToken(token, prisma);
return {
prisma,
user // Available in all resolvers
};
}
});
Protecting Resolvers
Check if a user is authenticated before allowing access to protected resources:
const resolvers = {
Query: {
me: (parent, args, { user }) => {
if (!user) {
throw new Error('Not authenticated');
}
return user;
},
myPosts: async (parent, args, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
return await prisma.post.findMany({
where: { authorId: user.id }
});
}
},
Mutation: {
createPost: async (parent, { title, content }, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
return await prisma.post.create({
data: {
title,
content,
authorId: user.id
}
});
},
deletePost: async (parent, { id }, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
// Check if user owns the post
const post = await prisma.post.findUnique({ where: { id } });
if (!post) {
throw new Error('Post not found');
}
if (post.authorId !== user.id) {
throw new Error('Not authorized to delete this post');
}
return await prisma.post.delete({ where: { id } });
}
}
};
Create a reusable helper function to check authentication and avoid repetition:
const requireAuth = (user) => { if (!user) throw new Error('Not authenticated'); };
Role-Based Access Control (RBAC)
Implement authorization based on user roles:
const resolvers = {
Query: {
users: async (parent, args, { user, prisma }) => {
// Only admins can list all users
if (!user || user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
return await prisma.user.findMany();
}
},
Mutation: {
deleteUser: async (parent, { id }, { user, prisma }) => {
// Only admins can delete users
if (!user || user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
return await prisma.user.delete({ where: { id } });
},
publishPost: async (parent, { id }, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
const post = await prisma.post.findUnique({ where: { id } });
// Authors can publish their own posts
// Editors and admins can publish any post
const canPublish =
post.authorId === user.id ||
user.role === 'EDITOR' ||
user.role === 'ADMIN';
if (!canPublish) {
throw new Error('Not authorized to publish this post');
}
return await prisma.post.update({
where: { id },
data: { published: true }
});
}
}
};
Directive-Based Authorization
Use custom directives for cleaner authorization logic:
const { SchemaDirectiveVisitor } = require('apollo-server-express');
// Define directive in schema
const typeDefs = `
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
USER
EDITOR
ADMIN
}
type Query {
me: User @auth
users: [User!]! @auth(requires: ADMIN)
myPosts: [Post!]! @auth
}
`;
// Implement directive
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { requires } = this.args;
field.resolve = async function(...args) {
const context = args[2];
const { user } = context;
if (!user) {
throw new Error('Not authenticated');
}
if (requires && user.role !== requires) {
throw new Error(`Requires ${requires} role`);
}
return resolve.apply(this, args);
};
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
auth: AuthDirective
}
});
Security Best Practices:
- Always hash passwords (never store plain text)
- Use strong JWT secrets (at least 256 bits)
- Set reasonable token expiration times
- Implement refresh tokens for long-lived sessions
- Validate all user inputs
- Use HTTPS in production
Field-Level Permissions
Hide sensitive fields based on user permissions:
const resolvers = {
User: {
email: (parent, args, { user }) => {
// Only show email to the user themselves or admins
if (user && (user.id === parent.id || user.role === 'ADMIN')) {
return parent.email;
}
return null;
},
password: () => {
// Never expose password hash
return null;
}
}
};
Complete Authentication Example
// Client usage
const LOGIN_MUTATION = `
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
id
email
name
}
}
}
`;
// After login, store token in localStorage
localStorage.setItem('token', data.login.token);
// Include token in all requests
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
headers: {
authorization: `Bearer ${localStorage.getItem('token')}`
}
});
Practice Exercise:
- Implement user registration and login mutations with JWT
- Create a protected query that requires authentication
- Add a
rolefield to users (USER, EDITOR, ADMIN) - Implement authorization to restrict post deletion to authors and admins only
- Create a field-level permission that hides user emails from non-admins
- Test your authentication by sending queries with and without valid tokens