Lesson 2
Authorization Strategies for Role-Based Access Control
Introduction to Role-Based Access Control (RBAC)

In this lesson, we'll delve into Role-Based Access Control (RBAC), a critical concept in securing applications. RBAC helps you manage user permissions based on their roles. This is important for maintaining security and ensuring that users can only access the data and functionalities they are authorized to use.

As a reminder from the previous lesson, we've already set up basic authentication on our GraphQL server using Apollo Server. Now, we will build on that foundation to implement more granular access control using roles.

Implementing Role-Based Access Control

To implement RBAC, we need to differentiate between user roles and permissions. For simplicity, we will use two roles: ADMIN and USER. Each role will have different permissions.

Here's an example dataset of users with their respective roles:

UsernamePasswordRole
adminadminADMIN
useruserUSER

Next, let's modify our context function to extract user roles based on a provided token.

TypeScript
1const users = [ 2 { username: 'admin', password: 'admin', role: 'ADMIN' }, 3 { username: 'user', password: 'user', role: 'USER' } 4]; 5 6const server = new ApolloServer({ 7 typeDefs, 8 resolvers, 9 context: ({ req }) => { 10 const token = req.headers.authorization || ''; 11 const user = users.find(user => user.username === token.split(' ')[1]); 12 return { user }; 13 } 14});

This code sets up an ApolloServer with user data and parses the authorization header from incoming requests to determine the user's identity and role. The users array defines users with a username, password, and role. The context function extracts the username from the Bearer <username> format token in the authorization header, finds the corresponding user in the users array, and returns the user object to be used in the resolvers for role-based access control.

Securing Mutations with RBAC

To secure mutations, we will use the login mutation to authenticate users and assign roles. We will then secure another mutation, addBook, ensuring only ADMIN users can add books.

TypeScript
1const typeDefs = gql` 2 type Book { 3 id: ID! 4 title: String! 5 author: String! 6 } 7 8 type User { 9 username: String! 10 role: String! 11 } 12 13 type Query { 14 books: [Book] 15 } 16 17 type Mutation { 18 login(username: String!, password: String!): User 19 addBook(title: String!, author: String!): Book 20 } 21`; 22 23const books = [ 24 { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' }, 25 { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' } 26]; 27 28const resolvers = { 29 Mutation: { 30 login: (_: any, { username, password }: { username: string, password: string }) => { 31 const user = users.find(user => user.username === username && user.password === password); 32 if (!user) { 33 throw new AuthenticationError('Invalid credentials'); 34 } 35 return { username: user.username, role: user.role }; 36 }, 37 addBook: (_: any, { title, author }: { title: string, author: string }, { user }: { user: { role: string } }) => { 38 if (user.role !== 'ADMIN') { 39 throw new AuthenticationError('You do not have permissions to add a book'); 40 } 41 const newBook = { id: '3', title, author }; 42 books.push(newBook); 43 return newBook; 44 }, 45 }, 46};

In this setup, we check if the user role is ADMIN before allowing them to add a book. If the user does not have the necessary permissions, an AuthenticationError is thrown.

Testing Role-Based Access Control: Login

Let's test our RBAC implementation using a separate script. First, we log in to get a token.

TypeScript
1const login = async (username: string, password: string): Promise<string | null> => { 2 const query = ` 3 mutation Login($username: String!, $password: String!) { 4 login(username: $username, password: $password) { 5 username 6 role 7 } 8 } 9 `; 10 const variables = { username, password }; 11 const response = await executeQuery(query, variables); 12 13 if (response.errors) { 14 console.error(response.errors); 15 return null; 16 } 17 18 return `Bearer ${username}`; 19};
Testing Role-Based Access Control: Add and Fetch Books

Next, we call mutations to add a new book and retrieve all books from the server.

TypeScript
1const addBook = async (title: string, author: string, token: string): Promise<any | null> => { 2 const query = ` 3 mutation AddBook($title: String!, $author: String!) { 4 addBook(title: $title, author: $author) { 5 id 6 title 7 author 8 } 9 } 10 `; 11 const variables = { title, author }; 12 const response = await executeQuery(query, variables, token); 13 14 if (response.errors) { 15 console.error(response.errors); 16 return null; 17 } 18 19 return response.data.addBook; 20}; 21 22const getBooks = async (token: string): Promise<any[] | null> => { 23 const query = ` 24 query { 25 books { 26 id 27 title 28 author 29 } 30 } 31 `; 32 const response = await executeQuery(query, {}, token); 33 34 if (response.errors) { 35 console.error(response.errors); 36 return null; 37 } 38 39 return response.data.books; 40};
Testing Role-Based Access Control: Putting All Together

Finally, let's put things together and call all these methods we've defined:

TypeScript
1(async () => { 2 const token = await login('admin', 'admin'); 3 if (!token) { 4 console.error('Failed to login'); 5 return; 6 } 7 8 console.log('Fetching books...'); 9 const books = await getBooks(token); 10 console.log('Books:', books); 11 12 console.log('Adding a new book...'); 13 const newBook = await addBook('1984', 'George Orwell', token); 14 console.log('Added book:', newBook); 15 16 console.log('Fetching books again...'); 17 const updatedBooks = await getBooks(token); 18 console.log('Books:', updatedBooks); 19})();

This example script demonstrates a complete flow: logging in as admin, adding a book, and fetching the list of books to validate the role-based access control implementation.

Lesson Summary

In this lesson, we successfully implemented Role-Based Access Control (RBAC) using Apollo Server. You learned how to:

  • Set up Apollo Server.
  • Implement basic authentication and role-based authorization.
  • Secure GraphQL mutations with RBAC.
  • Test the implementation using practical examples.

Next, you'll engage in practice exercises to reinforce these concepts. These hands-on activities will help solidify your understanding of RBAC and prepare you for more advanced topics. Keep exploring and experimenting with different scenarios to deepen your knowledge of securing GraphQL APIs.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.