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.
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:
Username | Password | Role |
---|---|---|
admin | admin | ADMIN |
user | user | USER |
Next, let's modify our context
function to extract user roles based on a provided token.
TypeScript1const 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.
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.
TypeScript1const 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.
Let's test our RBAC implementation using a separate script. First, we log in to get a token.
TypeScript1const 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};
Next, we call mutations to add a new book and retrieve all books from the server.
TypeScript1const 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};
Finally, let's put things together and call all these methods we've defined:
TypeScript1(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.
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.