Spring boot returns 403 although i have admin authority

Viewed 481

I came up with this issue when i added /admin endpoint to antMatchers("/admin").hasAuthority("ADMIN") it simply won't make a GET request to /admin and return 200 it returns 403 instead

Note: i'm using JWT as an extra layer of authentication.

This is my security config

httpSecurity.csrf().disable()
 .authorizeRequests().antMatchers("/", "/health", "/authority", "/dashboard", "/users/login", "/logoutUser", "/manageEvents", "/manageAeds", "/manageReports",
  "/charts", "/error", "/profile", "/authenticate/**", "/login", "/403", "/userProfile", "/deleteAed", "/users/add").permitAll()
 .antMatchers("/admin").hasAuthority("ADMIN")
 .antMatchers("/css/**", "/img/**", "/js/**", "/images/**", "/error_css/**", "/scss/**", "/vendor/**").permitAll()
 .anyRequest().authenticated().and().
exceptionHandling().accessDeniedPage("/403").and()
 .sessionManagement()
 .sessionCreationPolicy(SessionCreationPolicy.STATELESS);


httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

By moving it to permit.All() it will work but it's not the case here.

This is where i handle the redirect inside @Controller

@GetMapping("/authority")
public String getAuth(HttpServletResponse response) {

 if (jwt == null) {
  return "redirect:/login";
 }
 if (jwtTokenUtil.isTokenExpired(jwt)) {
  return "redirect:/login?token=expired";
 }
 response.addHeader("Auth", "Bearer " + jwt);

 System.out.println(loggedinUser.getRoles());

 if (loggedinUser != null) {
  if (loggedinUser.getRoles().equalsIgnoreCase("TILEFONITIS")) {
   return "redirect:/dashboard"; //will redirect
  } else if (loggedinUser.getRoles().equalsIgnoreCase("ADMIN")) {
   System.out.println("Admin");
   return "redirect:/admin"; //won't redirect
  } else if (loggedinUser.getRoles().equalsIgnoreCase("GUEST")) {
   return "redirect:/403"; // will redirect
  } else {
   return "redirect:/dashboard"; // will redirect
  }
 } else {
  return "redirect:/login";
 }

}

and this is my /admin inside @Controller which is never called.

@GetMapping("/admin")
public String getAdmin(HttpServletResponse response) {
 if (jwt == null) {
  return "redirect:/login";
 }
 if (jwtTokenUtil.isTokenExpired(jwt)) {
  return "redirect:/login?token=expired";
 }
 response.addHeader("Auth", "Bearer " + jwt);

 System.out.println("jwt" + jwt);

 return "admin";

}

The odd thing is that with Postman i get redirected!

What am i missing here?


Edit: The first call is at /authenticate/web where i tell spring i'm authenticated

authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(auth.getUsername(), auth.getPassword()));

Edit 2:

To make things even clearer:

Visiting from web, flow:

  1. POST /authenticate/web
  2. redirect with .js to /authority (GET)
  3. Won't redirect to /admin (GET) -> 403

Visiting from Postman, flow:

  1. POST /authenticate/web
  2. Get the JWT and include it in headers and make a GET to /authority
  3. I'm seeing the admin template. -> 200

That's really odd, i add the jwt every time with response.addHeader on the web flow .


Update:

  • These are the response headers from postman:

enter image description here

plus the JWT .

  • Response headers from the web

enter image description here

Although now i noticed i get 302 from the web instead of a 200

and as you can see admin page is 403

enter image description here


Update 2:

I've managed to break down a few things, first of all

  • by having a httpSecurity.addFilterBefore on my security configuration means spring will look for the JWT and add a filter before the position of the specified filter class

  • authorities are correctly assigned to users, so there is no issue there

  • i changed hasAuthority() to hasRole()

If you get the current user you can automatically access it's authority as shown below

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("Principal: "+auth.getPrincipal());
System.out.println("Authorities: "+auth.getAuthorities());             

Because the authentication is overriden by the jwt filter this means i will only manage to get a user only if the request header contains the valid jwt

this is why it worked from postman but not from the web.


Another issue is that in my controller i'm trying to add the jwt on the response header which will be added to it only when the controller finishes it's job, i can't get in the very next line the user principal becuase there is no jwt to it's request header.

This screenshot represents a web call and the call from postman where both access /authority endpoint.

enter image description here

  • From postman you see ADMIN as an authority
  • But from the web i have a ROLE_ANONYMOUS

So i have two options to solve this:

  1. Add it to the request header.
  2. Protect REST endpoints with JWT and use default spring security (hasRole() etc) for the web part.
4 Answers

After a lot of trial and error , i managed to break down a few things and make the security work with a JWT as well as the default spring authentication.

In order for it to work i had to change entirely my SecurityConfig , and alter it to MultipleSecurity.

This is how Multiple Security works:

with @Order(1) annotation the security configuration is marked as the first thing to look for.

@Order(1)
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

 @Bean
 public AuthenticationManager authenticationManagerBean() throws Exception {
  return super.authenticationManagerBean();
 }

 @Override
 protected void configure(HttpSecurity httpSecurity) throws Exception {
  httpSecurity.csrf().disable()
   .antMatcher("/api/**").authorizeRequests()
   .antMatchers("/api/authenticate/android").permitAll()
   .anyRequest().authenticated().and()
   .exceptionHandling().accessDeniedPage("/403").and().sessionManagement()
   .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
   .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
 }

}
  • @Order(1) JWT
  • @Order(2) default spring security
@Order(2)
@Configuration
public class SecurityConfiguration2 extends WebSecurityConfigurerAdapter {

 @Override
 protected void configure(HttpSecurity httpSecurity) throws Exception {
  httpSecurity.authorizeRequests()
   .antMatchers("/web/**").hasAuthority("ADMIN")
   .and().formLogin().defaultSuccessUrl("/redirect")
   .and().logout()
   .permitAll();
 }

}

What we basically say with these configurations is:

  1. Look for a JWT at /api/**
  2. Look for authorities at /web/**

So all the /web endpoints like /web/admin will now require authorities and not a JWT !

and all sensitive information from /api/** will require a JWT

  1. Enable the developer tools in your browser and capture the request and response headers of GET /authority from web. ie 2nd step of your web flow

  2. Capture the request and response headers of GET /authority from postman. ie. 2nd step of your postman flow

  3. Compare them (or post them here)

Update based on screenshots posted

Visiting from web:

  1. POST /authenticate/web - this is success
  2. redirect with .js to GET /authority - this is also a success because it did it's job of redirecting it to /admin.
    (response from authority redirects the browser to /admin so no issue here)
  3. auto redirected request to /admin fails with 403 (because as expected there is nobody add the jwt header to request as it is not a cookie or it is not part of the query param of the redirected url or some session session is maintained) (This is correct expected behaviour). See this question. How to forward headers on HTTP redirect

Visiting from Postman:

  1. POST /authenticate/web this is success
  2. Get the JWT and include it in headers and make a GET to /authority
  3. Postman redirects it to /admin like in web flow (Postman retains the original jwt headers and forwards it to redirected url too but it shouldn't be doing it)

Summary

In summary, postman and web are behaving differently on redirect.i.e on a redirect, postman sends the original headers (in your case, it is jwt) and browser does not retain original http headers.

So you have to redesign your application. For example,

  1. you can capture the response from /authority endpoint in javascript and manually forward it to /admin with http header
  2. or may be as cookies. Because it will sent automatically even on redirect

As Spring Security suggest mappings which are secured as to come before unsecured mappings, In this case,

.antMatchers("/admin").hasAuthority("ADMIN")

has to come before unsecured mappings("/","/authority" and others), In the Authorization part, this might be causing the problem.

.antMatchers("/admin").hasAuthority("ADMIN")

Instead of "/admin" use "/admin/**" it works

Related