4

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.
Phill Alexakis
  • 1,449
  • 1
  • 12
  • 31

4 Answers4

2

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

Phill Alexakis
  • 1,449
  • 1
  • 12
  • 31
1
  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
  • both contain my valid `JWT` at response headers, i'll update my question with them – Phill Alexakis Jun 18 '20 at 06:13
  • @phill-alexakis, as you can see the issue is not redirect in the web flow. Actually it also redirected to ```/admin``` endpoint using ```302``` status code. So now the question is why the redirected request ```/admin``` endpoint succeeds in postman and fails in web. –  Jun 18 '20 at 06:57
  • @phill-alexakis to understand that, can you capture all your request headers again from both postman and web. I.e you have the screenshot capturing the response headers of ```admin``` from web. But you didn't capture the request headers. I am pretty sure there is something different in the request headers of ```/admin``` (note the issue is not ```authority``` endpoint because in both flows, it did the redirect to ```/admin```) –  Jun 18 '20 at 07:01
  • yes i added it, but i dont't see a `JWT` meaning the controller in not called to assign it – Phill Alexakis Jun 18 '20 at 07:13
  • So you can see why the ```/admin``` is failing in webflow, because there is no jwt http header sent in http request. Are you manually adding the jwt header in postman when sending to ```/admin```? –  Jun 18 '20 at 07:21
  • 1
    By using postman the controller is somehow called and then assigns the `JWT`. From the web the controller is not called as a result not `JWT` is set at response headers – Phill Alexakis Jun 18 '20 at 07:26
  • responding to your summary , i'll try to forward the header manually , but there is no template for authority to be run to = no JavaScript to include at `/authority` , any ideas ? – Phill Alexakis Jun 18 '20 at 08:34
  • if you take a look at `/admin` endpoint , when it's called, i add the `response header` , but that endpoint is not called with the `redirect:/admin` when `/admin` has `has.Authority()` , again postman works , web doesn't – Phill Alexakis Jun 18 '20 at 08:40
  • what you add is response header so the response to the browser has it as response header. But now browser needs to make a new redirected request. And there is no requirement on browser to copy the response header to the redirected request –  Jun 18 '20 at 08:44
  • sure but, why with all other endpoints from the web are working fine? – Phill Alexakis Jun 18 '20 at 08:45
  • None of auto redirect endpoint that requires jwt token would have worked from browser. Can you point which endpoint worked? –  Jun 18 '20 at 08:53
  • yes, all the endpoints included at `.authorizeRequests().antMatchers(...).permitAll()` , (like `/authority` ) the logic goes as follows, when `/something` is called goes into the controller and get's the token to it's `HttpServletResponse` , so when the web renders it , the `request header` will contain the `JWT` but when it's at `.hasAuthority()` it won't ! but it will from postman! i think im losing my mind :D – Phill Alexakis Jun 18 '20 at 09:04
  • I believe it has to do more with the `authentication` part rather than the `token` – Phill Alexakis Jun 18 '20 at 09:08
  • There is nothing wrong in your ```spring-security``` config. Issue in your design is that you store them as response headers which doesn't get copied on redirect. You can store them as cookies instead. –  Jun 18 '20 at 09:12
0

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.

Nayan
  • 311
  • 1
  • 6
  • That's not the case...with Postman by following the flow i get authenticated and admin is returned , through web though it doesn't – Phill Alexakis Jun 17 '20 at 11:30
-1
.antMatchers("/admin").hasAuthority("ADMIN")

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

kkrkuldeep
  • 83
  • 5
  • it doesn't , `/admin` is 403 , why should `/admin/something` return 200? – Phill Alexakis Jun 17 '20 at 11:17
  • `System.out.println(loggedinUser.getRoles());` It prints..?? – kkrkuldeep Jun 17 '20 at 11:24
  • .antMatchers("/admin").access("hasRole('ROLE_ADMIN')") .. as `ROLE_` is the default Prefix you can use it or `ADMIN` can grant permission https://www.journaldev.com/8748/spring-security-role-based-access-authorization-example helps you – kkrkuldeep Jun 17 '20 at 11:38
  • i know it can grand permission like this, although it does with just `ADMIN` but not from web only by calling it from `postman` – Phill Alexakis Jun 17 '20 at 11:44
  • httpSecurity.csrf().disable() .authorizeRequests().antMatchers("/authority").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated().and() .exceptionHandling().and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); – kkrkuldeep Jun 17 '20 at 15:01