GraphQL

Authentication and Authorization

20 min Lesson 10 of 35

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:
  1. Implement user registration and login mutations with JWT
  2. Create a protected query that requires authentication
  3. Add a role field to users (USER, EDITOR, ADMIN)
  4. Implement authorization to restrict post deletion to authors and admins only
  5. Create a field-level permission that hides user emails from non-admins
  6. Test your authentication by sending queries with and without valid tokens