I am using spring boot 3 and spring security 6 and trying to configure authorization but not able to get it working. I am seeing some weird behaviour which I am not able to understand.
I have configured authorization using following code
http.authorizeHttpRequests(authrorize -> authrorize.requestMatchers("/hello/**").hasRole("USER"));
but after login I get 403 Forbidden error.
I checked the spring security logs and I can see following in the logs:
2024-06-15T06:55:20.123+05:30 DEBUG 15236 --- [spring-authn-authz] [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=com.nagpal.spring_authn_authz.config.EmployeeUserDetails@2af776cf, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=846F84FABBD0F9B98EFEF4DAAE047AA4], Granted Authorities=[USER]]]
Granted authority of USER is present but I am still getting 403 error.
After some search I found that, my authorities need to be prepended by "ROLE_", so I tried to append "ROLE_" prefix to the role from my UserDetails object:
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
log.info("Adding role with ROLE_ appended");
GrantedAuthority role = new SimpleGrantedAuthority("ROLE_" + employee.getRole());
authorities.add(role);
return authorities;
}
after this change, when I try to access the endpoint, I still get 403 Forbidden error but this in logs I can see ROLE_ prefix is appended twice:
2024-06-15T06:59:28.747+05:30 DEBUG 15236 --- [spring-authn-authz] [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=com.nagpal.spring_authn_authz.config.EmployeeUserDetails@2af776cf, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=846F84FABBD0F9B98EFEF4DAAE047AA4], Granted Authorities=[ROLE_ROLE_USER]]]
Here is code for EmployeeUserDetails
public class EmployeeUserDetails implements UserDetails {
private Employee employee;
public EmployeeUserDetails(Employee employee) {
this.employee = employee;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// log.info("Adding role with ROLE_ appended");
GrantedAuthority role = new SimpleGrantedAuthority(/*"ROLE_" + */ employee.getRole());
authorities.add(role);
return authorities;
}
@Override
public String getPassword() {
return employee.getPassword();
}
@Override
public String getUsername() {
return employee.getEmail();
}
and for Employee
@Entity
@Data
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "employee_id")
private int id;
@Column
private String email;
@Column
private String password;
@Column
private String role;
}
Creating users:
public void customUsers(UserDetailsManager detailsManager) {
Employee employee1 = new Employee();
employee1.setEmail("[email protected]");
employee1.setPassword(passwordEncoder().encode("password"));
employee1.setRole("USER");
EmployeeUserDetails user1 = new EmployeeUserDetails(employee1);
detailsManager.createUser(user1);
}
Employee table after startup:
I am unable to understand this behaviour. If i don't append "ROLE_" in my UserDetails objects it does not come at all but If i configure it it comes twice.
Can someone help me understand this?
By taking a closer look at Spring Security's hasRole code, you can see that it automatically adds ROLE_ to the role value for comparison.
Therefore, you need to add the ROLE_
prefix to the authorities inside the getAuthorities method.
public AuthorizeHttpRequestsConfigurer<H>.AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
return this.access(AuthorityAuthorizationManager.hasRole(role));
}
public static <T> AuthorityAuthorizationManager<T> hasRole(String role) {
Assert.notNull(role, "role cannot be null");
return hasAuthority("ROLE_" + role);
}
When I replicated your code, it worked correctly in my case.😱
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority role = new SimpleGrantedAuthority("ROLE_" + this.role);
log.info("Adding role : " + role.getAuthority());
authorities.add(role);
return authorities;
}
2024-06-15T14:20:01.450+09:00 INFO 22987 --- [nio-8080-exec-6] com.fixadate.global.util.LogAspect : timeMs = 21
2024-06-15T14:20:01.450+09:00 INFO 22987 --- [nio-8080-exec-6] c.fixadate.domain.member.entity.Member : Adding role : ROLE_USER
.authorizeHttpRequests((req) -> req
.requestMatchers(PERMIT_URL_ARRAY).permitAll()
.requestMatchers("/hello/**").hasRole("USER")
.anyRequest().authenticated())
To help you, I'll explain how I assign roles.
You can easily grant authorities using the commaSeparatedStringToAuthorityList method.
Let me explain with the code.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_" + role);
}
Using a comma, you can easily assign multiple roles.
For example, you can store the values USER,MEMBER
in the role of an Entity. This way, the Entity will have the authorities ROLE_USER
and ROLE_MEMBER
.
By looking into the internal logic, you can see that it splits the string by commas and adds each as a role.
public static List<GrantedAuthority> commaSeparatedStringToAuthorityList(String authorityString) {
return createAuthorityList(StringUtils.tokenizeToStringArray(authorityString, ","));
}
public static List<GrantedAuthority> createAuthorityList(String... authorities) {
List<GrantedAuthority> grantedAuthorities = new ArrayList(authorities.length);
String[] var2 = authorities;
int var3 = authorities.length;
for(int var4 = 0; var4 < var3; ++var4) {
String authority = var2[var4];
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
}
return grantedAuthorities;
}
Since unexpected issues can occur when adding or removing the ROLE_
prefix in the getAuthorities method, I resolved the problem by adding the ROLE_
prefix in the setRole method.
public void setRole(String role) {
this.role = "ROLE_" + role;
}
I'm so happy that this issue is resolved.
have a nice day - kevin