7

Spring security configuration class

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .cors()
            .and()
            .authorizeRequests()
            .antMatchers("/user", "/login").permitAll()
            .antMatchers("/employee", "/insurance").hasRole("User")
            .anyRequest()
            .authenticated()
            .and()
            .httpBasic()
            .and()
            .csrf().disable();
    }

    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }
}

UserDetailsService Implementation class

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User user = null;
        Set<GrantedAuthority> grantedAuthorities = null;
        try
        {
            user = userService.findByUserName(userName);
            if(user == null)
                throw new UsernameNotFoundException("User " + userName  + " not available");

            grantedAuthorities = new HashSet<>();
            for(Role role: user.getRoles()) {
                grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole().toString()));
            }
        }
        catch(Exception exp) {
            exp.printStackTrace();
        }
        return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), grantedAuthorities);
    }
}

Employee Rest Controller class

@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @Autowired
    private InsuranceService insuranceService;

    @PostMapping("/employee")
    public ResponseEntity<Employee> create(@RequestBody Employee employee) throws Exception {
        employee = employeeService.create(employee);
        return new ResponseEntity<Employee>(employee, HttpStatus.CREATED);
    }

    @PutMapping("/employee")
    public ResponseEntity<Employee> update(@RequestBody Employee employee) throws Exception {
        employee = employeeService.update(employee);
        return new ResponseEntity<Employee>(employee, HttpStatus.OK);
    }

    @DeleteMapping("/employee/{id}")
    public ResponseEntity<String> delete(@PathVariable("id") long id) throws Exception {
        employeeService.delete(id);
        return new ResponseEntity<String>("Employee deleted successfully", HttpStatus.OK);
    }

    @GetMapping("/employee/{id}")
    public ResponseEntity<Employee> findEmployeeDetails(@PathVariable("id") long id) throws Exception {
        Employee employee = employeeService.findById(id);
        return new ResponseEntity<Employee>(employee, HttpStatus.OK);
    }

    @GetMapping("/employee")
    public ResponseEntity<List<Employee>> findAll() throws Exception {
        List<Employee> employees = employeeService.findAll();
        return new ResponseEntity<List<Employee>>(employees, HttpStatus.OK);
    }
}

I am getting 403 forbidden error for any of the HTTP method(POST/GET/PUT) request submitted via postman to /employee URL

{
    "timestamp": "2019-09-17T05:37:35.778+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/hr-core/employee"
}

I am getting this error even though I am sending correct username & password in the basic auth header(Authorization) of HTTP request in POSTMAN. This user is also having both USER and ADMIN roles to access /employee REST endpoint. I have disabled CSRF in http security.

How can I solve this error?

Karthik
  • 1,302
  • 5
  • 25
  • 56

2 Answers2

14

Within Spring Security, there is a difference between roles and authorities. While an authority can be anything, roles are a subset of authorities that start with ROLE_.

Let's say you have the following authorities:

GrantedAuthority authority1 = new SimpleGrantedAuthority("User");
GrantedAuthority authority2 = new SimpleGrantedAuthority("ROLE_Admin");

In this case, authority1 does not contain a role, while authority2 does because it's prefixed with ROLE_.

That means, that if you use hasRole("User"), you won't have access, because it's not defined as a role. hasRole("Admin") on the other hand would work.

To solve this, you have two options:

  1. Make sure your roles are really prefixed with ROLE_. If you don't store them that way in your database, you can modify your UserDetailsServiceImpl:

    String roleName = "ROLE_" + role.getRole().toString();
    grantedAuthorities.add(new SimpleGrantedAuthority(roleName));
    
  2. Alternatively, you can use hasAuthority("User") instead:

    // ...
    .antMatchers("/employee", "/insurance").hasAuthority("User")
    // ...
    
g00glen00b
  • 41,995
  • 13
  • 95
  • 133
  • Thanks for your answer. I just wanted to let you know that after updating .antMatchers("/hr-core/employee/**", "/hr-core/insurance/**").hasRole("User") in security config class, the REST endpoint is accessible now. But I am not sure if hard coding context path in the security class is the right approach. – Karthik Sep 17 '19 at 07:27
  • @Karthik You shouldn't have to do that. Your original antmatcher worked fine (except that you could add `/**` at the end if all subpaths should also work). Now you've invalidated that antmatcher since it will now look for `/hr-core/hr-core/employee/**`. Since it doesn't match, it will fall back to `anyRequest().authenticated()`. So right now, your role-based authorization is bypassed (you can test this by trying to access the endpoint without having the proper `User` role). – g00glen00b Sep 17 '19 at 07:36
  • You are absolutely right. The role-based authorization was getting bypassed. I will update my code as per your answer and check. Thanks again – Karthik Sep 17 '19 at 07:43
  • you save my day! – Hafiz Hamza Apr 25 '20 at 10:16
0

This is how I remove the error for my REST API access. When I am calling the API, it giving me 403 error. To solve this I made these changes.

  1. Instead of antMatcher, I used mvcMatcher for API mapping
  2. Roles are provided as names like "USER" or "ADMIN" instead of "ROLE_USER" or "ROLE_ADMIN"

Here is the code:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .csrf().disable().authorizeRequests()
        .and()
        .addFilter(new ApplicationAuthorizationFilter(authenticationManager()))
        .authorizeRequests()
        .antMatchers(ApplicationConstants.DEFAULT_API_CHECK_PATH).permitAll()
        .mvcMatchers("/app/users/**/**").hasAnyRole("USER", "ADMIN")
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("abc").password("xyz").roles("READONLY")  ;
    }
    
    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder(ApplicationConstants.ENCODER_STRENGTH);
    }
    
    
}

There is mechanism available in Spring Security to whether the prefix should be added or not before the GrantedAuthotrity value and what it should be. Default is blank as I have not set anything in my case.

Earlier I was trying to pass the role name as "ROLE_USER" and it was failing.

Atul
  • 3,043
  • 27
  • 39