Role-Based Authorization in Spring Boot Project
Role-Based Access Control (RBAC) is a fundamental security mechanism that restricts system access to authorized users based on assigned roles. While authentication verifies who a user is, authorization determines what an authenticated user is allowed to do. This chapter walks through designing, implementing, and enforcing role-based authorization in a Spring Boot application backed by MongoDB and Spring Security.
1. The Problem: Authentication vs. Authorization
In basic authentication implementations, any client with valid credentials (a correct username and password combination) can access every endpoint that requires authentication.
Consider an administration endpoint designed to retrieve all registered users from the database (GET /admin/all-users):
The Inherent Vulnerability
If both a standard user (e.g., ram) and a system administrator (e.g., admin) pass the authentication check, a standard user could execute the GET /admin/all-users endpoint. This exposes sensitive details—including encrypted password hashes, user metadata, and private journal entries—to unauthorized individuals.
To fix this, authentication must be coupled with Role-Based Authorization (RBAC):
- Authentication: Verifies identity (
Username + Password). - Authorization: Verifies authority (
Role == ADMIN).
2. Core Mental Model & Real-Life Analogy
Real-Life Analogy: Corporate Building Security
- Authentication (Building Badge): When you swipe your employee badge at the front door, the scanner confirms your identity and lets you inside the lobby.
- Authorization (Door Clearance): When you walk up to the server room, your badge is checked again. Even though you are inside the building, only badges with Server Room clearance will unlock the door.
In software terms:
- Users (
ram,vipul,akshit) are individual accounts. - Roles (
USER,ADMIN) are privilege buckets assigned to accounts. - Multiple users can share the same role (, ).
3. Data Model Design: Roles in MongoDB & the User POJO
To support RBAC, the application's underlying data structures must store role information alongside user credentials.
The User Entity Class
The User POJO contains a roles field represented as a List<String>.
package com.example.journalApp.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.index.Indexed;
import java.util.ArrayList;
import java.util.List;
@Document(collection = "users")
@Data
@NoArgsConstructor
public class User {
@Id
private ObjectId id;
@Indexed(unique = true)
private String username;
private String password;
@DBRef
private List<JournalEntry> journalEntries = new ArrayList<>();
private List<String> roles = new ArrayList<>();
}
Document Representation in MongoDB
Inside the MongoDB users collection, roles are persisted as an array of strings:
{
"_id": { "$oid": "65f123456789abcdef012345" },
"username": "vipul",
"password": "$2a$10$e8K...",
"journalEntries": [],
"roles": ["USER", "ADMIN"]
}
4. Implementing the AdminController
The AdminController exposes endpoints restricted strictly to administrative tasks, such as fetching all user documents from the database.
package com.example.journalApp.controller;
import com.example.journalApp.entity.User;
import com.example.journalApp.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private UserService userService;
@GetMapping("/all-users")
public ResponseEntity<?> getAllUsers() {
List<User> all = userService.getAll();
if (all != null && !all.isEmpty()) {
return new ResponseEntity<>(all, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
Note on Naming Conventions: Avoid combining camelCase or underscores in URL paths (e.g.,
/all_users). Standard REST guidelines recommend kebab-case or lower-case endpoints (e.g.,/admin/all-users).
5. Configuring Spring Security for Role Enforcement
To secure endpoints under /admin/**, configure Spring Security's filter chain to evaluate user roles.
Security Configuration Snippet
package com.example.journalApp.config;
import com.example.journalApp.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SpringSecurity {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/journal/**", "/user/**").authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.httpBasic(httpBasic -> {});
return http.build();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
}
Important Constraint: Spring Security automatically prepends the prefix
ROLE_when evaluating methods like.hasRole("ADMIN"). It evaluates whether the authenticated user possesses the authorityROLE_ADMIN.
6. How Spring Security Evaluates Roles Under the Hood
When an incoming HTTP request hits an authorization-guarded route, Spring Security executes the following evaluation flow:
Implementing UserDetailsServiceImpl
The UserDetailsService bridges the MongoDB user records and Spring Security's authority framework:
package com.example.journalApp.service;
import com.example.journalApp.entity.User;
import com.example.journalApp.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user != null) {
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
7. The Bootstrapping Dilemma: Creating the First Admin
A common challenge arises when protecting admin creation endpoints:
- The
/admin/**routes are configured to require theADMINrole. - If no user in the database possesses the
ADMINrole, no user can access/admin/create-admin-user. - Consequently, no new admins can be created via the API.
Solution: Manual Database Bootstrapping
To break this cycle, manually assign the ADMIN role to an existing user directly inside MongoDB.
Step 1: Manual Update in MongoDB Compass / Shell
Locate a standard user (e.g., vipul) and update the document:
db.users.updateOne(
{ username: "vipul" },
{ $set: { roles: ["USER", "ADMIN"] } }
);
Step 2: Verification
Once updated, vipul possesses the necessary authority (ROLE_ADMIN) to access administrative endpoints and create additional admin users programmatically.
8. Admin Provisioning Workflow (saveAdmin in UserService)
Once an initial admin exists, administrative creation endpoints can be exposed securely within AdminController.
Service Layer Implementation
Add a dedicated saveAdmin method in UserService to explicitly attach both USER and ADMIN roles before saving to MongoDB:
package com.example.journalApp.service;
import com.example.journalApp.entity.User;
import com.example.journalApp.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public void saveUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRoles(Arrays.asList("USER"));
userRepository.save(user);
}
public void saveAdmin(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRoles(Arrays.asList("USER", "ADMIN"));
userRepository.save(user);
}
public List<User> getAll() {
return userRepository.findAll();
}
}
Updating AdminController
Add the administrative creation endpoint:
@PostMapping("/create-admin-user")
public ResponseEntity<?> createAdminUser(@RequestBody User user) {
try {
userService.saveAdmin(user);
return new ResponseEntity<>(user, HttpStatus.CREATED);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
9. End-to-End Execution & Testing Workflow
Step-by-Step Walkthrough
Step 1: HTTP POST /user (Public creation)
Payload: { "username": "akshit", "password": "password123" }
Result: Saved with roles = ["USER"]
Step 2: HTTP GET /admin/all-users
Auth Credentials: akshit : password123
Result: 403 FORBIDDEN (akshit is not an admin)
Step 3: HTTP POST /admin/create-admin-user
Auth Credentials: vipul : password123 (Vipul was bootstrapped with "ADMIN")
Payload: { "username": "akshit", "password": "password123" }
Result: 201 CREATED (Akshit re-saved/promoted with roles = ["USER", "ADMIN"])
Step 4: HTTP GET /admin/all-users
Auth Credentials: akshit : password123
Result: 200 OK (Returns list of all user documents)
10. Key Security Responses: 401 Unauthorized vs 403 Forbidden
Understanding HTTP status codes returned by Spring Security helps diagnose configuration and authentication issues:
11. Summary and Key Takeaways
- Authentication vs. Authorization: Authentication verifies identity (
who you are), whereas Authorization enforces permissions (what you can access). - Spring Security Authority Mapping: Using
.hasRole("ADMIN")automatically expects theGrantedAuthoritystring to be prefixed asROLE_ADMIN. - Array-Based Roles in MongoDB: Roles are stored as a
List<String>within theUserdocument, allowing a user to possess multiple roles concurrently (e.g.,["USER", "ADMIN"]). - The Bootstrapping Problem: Creating the first admin user requires manual database modification or seed scripts, because secured admin endpoints cannot be accessed without an existing admin account.
- Separation of Concerns: Endpoint access is enforced at the Security Filter Chain layer (
HttpSecurity), separating security rules from core business logic in controllers. - HTTP Status Clarity:
401 UnauthorizedUnauthenticated / bad credentials.403 ForbiddenAuthenticated, but insufficient privileges.