Search code examples
spring-bootspring-securityauthorization

Spring security 6 - authorization not working


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:

enter image description here

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?


Solution

  • 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.

    hasRole

    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.😱

    Entity

    @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;
    }
    

    log

    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
    

    SecurityConfig

    .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.

    AuthorityUtils.commaSeparatedStringToAuthorityList

    @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.

    commaSeparatedStringToAuthorityList

    public static List<GrantedAuthority> commaSeparatedStringToAuthorityList(String authorityString) {
        return createAuthorityList(StringUtils.tokenizeToStringArray(authorityString, ","));
    }
    

    createAuthorityList

    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;
    }
    

    Solution

    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.

    Employee

    public void setRole(String role) { 
        this.role = "ROLE_" + role; 
    }
    

    I'm so happy that this issue is resolved.

    have a nice day - kevin