Role vs GrantedAuthority: The Difference Explained
You set a user’s role to “ADMIN” in your database. You configure .hasRole("ADMIN") in your security chain. You log in.
403 Forbidden.
I spent an entire afternoon debugging this exact scenario three years ago. I was furious. The database said “ADMIN”. The code said “ADMIN”. Why was Spring Security blocking me?
Turns out, I didn’t understand the difference between a Role and a GrantedAuthority. Most developers I mentor at www.codegigs.app treat them as the same thing. They aren’t. Mixing them up is the #1 cause of authorization bugs in Spring Boot applications.
Here is how to stop guessing and actually fix your permission logic.
The Prefix Trap: Why hasRole() Fails
Here is the short version: Spring Security is obsessed with the string ROLE_.
When you use hasRole("ADMIN"), Spring doesn’t look for “ADMIN”. It looks for “ROLE_ADMIN”. If your database or token just has “ADMIN”, the check fails. Silence. No error message. Just a 403.
You have two choices: change your data or change your code. Here is the code-first fix.
The Security Configuration
// SecurityConfig.java // Spring Boot 3.2.0, Spring Security 6.2.0 package app.codegigs.security;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain;
@Configuration public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Option 1: hasRole automatically adds "ROLE_" prefix
// Checks for authority "ROLE_ADMIN"
.requestMatchers("/admin/**").hasRole("ADMIN")
// Option 2: hasAuthority checks for the EXACT string
// Checks for authority "ADMIN" (if that's what is in your DB)
.requestMatchers("/management/**").hasAuthority("ADMIN")
// Option 3: Granular authorities (Best Practice)
.requestMatchers("/products/delete/**").hasAuthority("product:delete")
.anyRequest().authenticated()
);
return http.build();
}
}
Code Breakdown
Line 18 is where the magic (or the bug) happens. hasRole("ADMIN") is just a helper method. It takes your string, prepends ROLE_, and calls hasAuthority(). That’s it. There is no special “Role” object in the Spring Security core.
Line 22 shows the raw approach. hasAuthority("ADMIN") checks for the string exactly as it appears. If your legacy database stores roles without prefixes, use this. But don’t mix them up, or you’ll create a maintenance nightmare.
Line 25 is the production pattern we teach at www.codegigs.app. Don’t secure endpoints with roles. Secure them with capabilities (like product:delete). Roles change. Capabilities rarely do.
So, What is a GrantedAuthority?
Technically? It’s an interface with one method: getAuthority(). That’s it.
It returns a String. Spring Security doesn’t care if that string is “ROLE_ADMIN”, “can_dance_salsa”, or “SCOPE_read”. It just compares strings.
Think of a GrantedAuthority as a single key to a specific door. A Role is just a keychain containing multiple keys. But in Spring’s eyes, they are both just strings in a list.
How to Load Authorities Correctly
You define these authorities when you load the user. If you screw this up here, your controller checks will never pass.
// CustomUserDetailsService.java package app.codegigs.security;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; 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;
import java.util.ArrayList; import java.util.List;
@Service public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Pseudo-code: Fetch user from your DB
// UserEntity userEntity = userRepository.findByUsername(username);
// Let's say the user has a "MANAGER" role in the DB
String dbRole = "MANAGER";
List<GrantedAuthority> authorities = new ArrayList<>();
// CRITICAL: We manually add the prefix here so hasRole("MANAGER") works
authorities.add(new SimpleGrantedAuthority("ROLE_" + dbRole));
// We also add a granular permission
authorities.add(new SimpleGrantedAuthority("invoice:approve"));
return new User(
"johndoe",
"{noop}password123", // Don't use {noop} in production!
authorities
);
}
}
Lines 28-29 are crucial. We are mapping a database value (“MANAGER”) to a Spring Security Role (“ROLE_MANAGER”) AND adding a granular permission (“invoice:approve”).
Now, in your controller, you can use @PreAuthorize("hasRole('MANAGER')") OR @PreAuthorize("hasAuthority('invoice:approve')"). Both work.
I saw a thread on r/java last month where a dev argued that SimpleGrantedAuthority was deprecated. It’s not. It’s still the standard implementation for 99% of use cases. Don’t overcomplicate it by implementing the interface yourself unless you have a weird requirement.
Common Mistake: The “Double Prefix”
Here is a funny way to break your app. You store “ROLE_USER” in the database (good). Then in your UserDetailsService, you do this:
authorities.add(new SimpleGrantedAuthority("ROLE_" + dbRole));
Result? Your authority is now ROLE_ROLE_USER. Your hasRole("USER") check looks for ROLE_USER. They don’t match. Access denied. I’ve done this. You’ve probably done this. It’s a rite of passage.
Always log your user’s authorities on startup or login failure. It saves hours.
Advanced: Role Hierarchy
So you have ADMIN and USER. Logic dictates that an Admin can do everything a User can do. But Spring doesn’t know that. By default, if an endpoint requires ROLE_USER, an ADMIN gets blocked.
You could add both authorities to the Admin user. Or, you can use a RoleHierarchy.
// RoleHierarchyConfig.java package app.codegigs.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
@Configuration public class RoleHierarchyConfig {
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
// ADMIN implies USER, which implies GUEST
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > ROLE_GUEST");
return hierarchy;
}
}
This bean tells Spring Security: “If they have ADMIN, treat them as if they also have USER.” This keeps your database clean—you only assign the top-level role, and the code handles the rest.
Which One Should You Use?
If you are building a blog? Use Roles. Simple.
If you are building a SaaS platform? Use Authorities. You will eventually need to let a “Viewer” also “Edit Comments” but not “Delete Posts”. Roles crumble under that complexity.
This is a core part of the architectural strategy we cover at www.codegigs.app. Start with roles, but design your code to check for authorities (capabilities). It decouples your code from your business titles.
If you’re still struggling with how these connect to the database, check out our guide on Authentication Providers. It explains where these authorities actually come from.
Master Spring Security 6
Tired of copy-pasting configs that you don’t understand? Join 50,000+ developers building secure systems.
Get the full Spring Security Masterclass at www.codegigs.app →