Building a Secure RBAC System in Spring Boot with MongoDB

Kamal Kumar Sha
7 min readSep 23, 2024

--

Introduction

Have you ever built an application and suddenly realized you need to manage user access in a more granular way? Maybe you’ve hardcoded some admin checks or scattered permission logic throughout your codebase. Trust me, I’ve been there, and it’s not fun to maintain.

That’s where Role-Based Access Control (RBAC) comes in. It’s a standardized way to manage user permissions based on their roles, making your application more secure and easier to maintain. In this post, I’ll walk you through implementing RBAC in a Spring Boot application using MongoDB. We’ll cover everything from setting up the project to securing your endpoints.

Prerequisites

Before we dive in, make sure you have the following set up:

  • Java Development Kit (JDK) 17 or higher: Spring Boot 3.x requires Java 17+.
  • Spring Boot 3.x
  • Spring Security 6.x
  • MongoDB 6.x

You’ll also need a basic understanding of:

  • Java programming
  • Spring Framework
  • MongoDB

Setting Up the Project

1. Create a New Spring Boot Project

First things first, let’s set up our Spring Boot project. You can use Spring Initializr or your favorite IDE. Include the following dependencies:

  • Spring Web
  • Spring Security
  • Spring Data MongoDB
  • Lombok (optional but highly recommended to reduce boilerplate code)

2. Update pom.xml

Make sure your pom.xml file includes the necessary dependencies:

<project ...>
<!-- ... other configurations ... -->
    <dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Data MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- Lombok (Optional) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!-- ... other dependencies ... -->
</dependencies>
<!-- ... other configurations ... -->
</project>

Configuring MongoDB

1. Add MongoDB Connection Details

We’ll need to connect our application to MongoDB. In your application.properties or application.yml, add the following:

application.properties

spring.data.mongodb.uri=mongodb://localhost:27017/rbac_db

Feel free to replace rbac_db with your preferred database name.

Defining the Entities

Now, let’s define the core entities for our RBAC system: Permission, Role, and User.

1. Permission.java

package com.example.rbac.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
@Data
@Document(collection = "permissions")
public class Permission {
@Id
private String id;
private String name;
}

2. Role.java

package com.example.rbac.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
import java.util.Set;
@Data
@Document(collection = "roles")
public class Role {
@Id
private String id;
private String name;
private Set<Permission> permissions;
}

3. User.java

package com.example.rbac.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
import java.util.Set;
@Data
@Document(collection = "users")
public class User {
@Id
private String id;
private String username;
private String password; // We'll store hashed passwords
private Set<Role> roles;
}

A Quick Note: Always store passwords in a hashed format. We’ll cover how to hash passwords later on.

Creating the Repositories

Repositories are the bridge between our application and the database. Let’s create them for our entities.

1. UserRepository.java

package com.example.rbac.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import com.example.rbac.model.User;
public interface UserRepository extends MongoRepository<User, String> {
User findByUsername(String username);
}

2. RoleRepository.java

package com.example.rbac.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import com.example.rbac.model.Role;
public interface RoleRepository extends MongoRepository<Role, String> {
Role findByName(String name);
}

3. PermissionRepository.java

package com.example.rbac.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import com.example.rbac.model.Permission;
public interface PermissionRepository extends MongoRepository<Permission, String> {
Permission findByName(String name);
}

Implementing UserDetailsService

To integrate with Spring Security, we’ll implement a custom UserDetailsService.

CustomUserDetailsService.java

package com.example.rbac.service;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import com.example.rbac.model.User;
import com.example.rbac.model.Role;
import com.example.rbac.model.Permission;
import com.example.rbac.repository.UserRepository;
import java.util.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
    @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
       return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthorities(user.getRoles())
);
}
    private Collection<SimpleGrantedAuthority> getAuthorities(Set<Role> roles) {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
for (Permission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getName()));
}
}
return authorities;
}
}

What’s Happening Here?

  • We fetch the user from the database using their username.
  • We construct a UserDetails object that Spring Security can use for authentication.
  • We convert roles and permissions into a collection of GrantedAuthority.

Configuring Spring Security

Now, let’s set up Spring Security to use our custom UserDetailsService.

SecurityConfig.java

package com.example.rbac.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import com.example.rbac.service.CustomUserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService; public SecurityConfig(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login").permitAll()
)
.logout(logout -> logout.permitAll());
return http.build();
}
    @Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http
.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.build();
}
    @Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Key Points:

  • Disabling CSRF: For simplicity, we’re disabling CSRF. In a production environment, make sure to configure CSRF protection properly.
  • Authorization Rules:
  • /admin/** endpoints require the ADMIN role.
  • /user/** endpoints require the USER role.
  • All other requests require authentication.
  • Form Login: We specify a custom login page at /login.

Managing Passwords Securely

Security is paramount, especially when it comes to user passwords. Let’s ensure we’re hashing passwords before storing them.

UserService.java

package com.example.rbac.service;
import org.springframework.stereotype.Service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.example.rbac.model.User;
import com.example.rbac.repository.UserRepository;
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
    public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
    public void saveUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user);
}
}

Why BCrypt?

BCrypt is a popular hashing algorithm that’s designed for hashing passwords. It incorporates a salt to protect against rainbow table attacks and is computationally intensive to guard against brute-force attacks.

Defining Controllers and Endpoints

Time to set up our REST controllers to handle incoming requests.

1. AdminController.java

package com.example.rbac.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("/dashboard")
public String adminDashboard() {
return "Welcome to the Admin Dashboard!";
}
}

2. UserController.java

package com.example.rbac.controller;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/profile")
public String userProfile() {
return "Welcome to your Profile!";
}
}

Handling Authentication

We’ll create a simple controller to handle login requests.

WebController.java

package com.example.rbac.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebController {
@GetMapping("/login")
public String login() {
return "login"; // This should correspond to a Thymeleaf template
}
}

Don’t Forget the View!

Make sure you have a login.html template under src/main/resources/templates/ if you're using Thymeleaf.

Creating Initial Data

To test our application, let’s pre-load some roles, permissions, and users.

DataLoader.java

package com.example.rbac.config;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.example.rbac.repository.RoleRepository;
import com.example.rbac.repository.PermissionRepository;
import com.example.rbac.repository.UserRepository;
import com.example.rbac.model.Permission;
import com.example.rbac.model.Role;
import com.example.rbac.model.User;
import com.example.rbac.service.UserService;
import java.util.Set;
@Component
public class DataLoader implements CommandLineRunner {
    private final RoleRepository roleRepository;
private final PermissionRepository permissionRepository;
private final UserService userService;
public DataLoader(RoleRepository roleRepository, PermissionRepository permissionRepository, UserService userService) {
this.roleRepository = roleRepository;
this.permissionRepository = permissionRepository;
this.userService = userService;
}
@Override
public void run(String... args) throws Exception {
// Create Permissions
Permission readPermission = new Permission();
readPermission.setName("READ_PRIVILEGE");
permissionRepository.save(readPermission);
Permission writePermission = new Permission();
writePermission.setName("WRITE_PRIVILEGE");
permissionRepository.save(writePermission);
        // Create Roles
Role adminRole = new Role();
adminRole.setName("ADMIN");
adminRole.setPermissions(Set.of(readPermission, writePermission));
roleRepository.save(adminRole);
        Role userRole = new Role();
userRole.setName("USER");
userRole.setPermissions(Set.of(readPermission));
roleRepository.save(userRole);
// Create Users
User adminUser = new User();
adminUser.setUsername("admin");
adminUser.setPassword("admin123"); // Password will be hashed
adminUser.setRoles(Set.of(adminRole));
userService.saveUser(adminUser);
User normalUser = new User();
normalUser.setUsername("user");
normalUser.setPassword("user123"); // Password will be hashed
normalUser.setRoles(Set.of(userRole));
userService.saveUser(normalUser);
}
}

What’s Going On?

  • We create two permissions: READ_PRIVILEGE and WRITE_PRIVILEGE.
  • We create two roles: ADMIN (with both permissions) and USER (with read permission).
  • We create two users: an admin user and a normal user.

Testing the Application

Let’s make sure everything works as expected.

1. Run the Application

Start your Spring Boot application:

mvn spring-boot:run

2. Access the Login Page

Navigate to http://localhost:8080/login. You should see your login page.

3. Test User Authentication

Admin User

  • Username: admin
  • Password: admin123

After logging in, try accessing:

Normal User

  • Username: user
  • Password: user123

After logging in, try accessing:

Conclusion

And there you have it! We’ve built a simple yet robust RBAC system using Spring Boot and MongoDB. Here’s a quick recap of what we’ve accomplished:

  • Set Up the Project: Initialized a Spring Boot project with the necessary dependencies.
  • Configured MongoDB: Connected our application to a MongoDB database.
  • Defined Entities: Created User, Role, and Permission models.
  • Created Repositories: Set up repositories for data access.
  • Implemented UserDetailsService: Integrated our user model with Spring Security.
  • Configured Spring Security: Set up authentication and authorization rules.
  • Managed Passwords Securely: Used BCrypt to hash passwords.
  • Defined Controllers: Created endpoints for different roles.
  • Created Initial Data: Pre-loaded roles, permissions, and users for testing.
  • Tested the Application: Verified that our RBAC system works as expected.

Next Steps:

Feel free to extend this application by adding more roles, permissions, and secure endpoints. You could also integrate JWT for stateless authentication or add a frontend to interact with your API.

Additional Best Practices

While we’ve covered the basics, here are some best practices to consider:

  • Validation: Use annotations like @NotNull and @Size to validate user input.
  • Exception Handling: Implement global exception handling with @ControllerAdvice.
  • Logging: Utilize a logging framework for better traceability.
  • Security Headers: Configure headers to protect against common vulnerabilities.
  • CORS Configuration: If you have a frontend application, set up Cross-Origin Resource Sharing appropriately.

Thank you for reading! If you found this post helpful, feel free to share it with others or leave a comment below. Happy coding!

Disclaimer: Always ensure your dependencies are up-to-date and refer to the official documentation for any version-specific features or configurations.

--

--