Spring Security - when get login page, security try to authenticate and return 401 error

Viewed 280

I am developing Spring boot application with microservices architecture. I am using JWT authentication.

1-http://localhost:8762/auth {"username":"admin", "password":"12345"} (POST request)

2-http://localhost:8762/auth/loginPage (GET request for page)

When i try first request, authentication is working well and i get login info and jwt token. But when i try second request for getting login page, spring is trying to authenticate and returns 401 error.

How can i ignore authentication for login page.

I have zull project as gateway and authentication project as auth.

if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
            chain.doFilter(request, response);          // If not valid, go to the next filter.
            return;
        }

I think at this point, i have to override filter. But i don't know how i write filter.

Here is my code for authentication.

auth project -> WebSecurityConfigurerAdapter

@EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtConfig jwtConfig;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
             // make sure we use stateless session; session won't be used to store user's state.
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                // handle an authorized attempts 
            .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and()
            // Add a filter to validate user credentials and add token in the response header
            
            // What's the authenticationManager()? 
            // An object provided by WebSecurityConfigurerAdapter, used to authenticate the user passing user's credentials
            // The filter needs this auth manager to authenticate the user.
            .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig()))    
            .authorizeRequests()
            // allow all POST requests 
            .antMatchers("/auth/**").permitAll()
            .antMatchers("/user/register").permitAll()
            // any other requests must be authenticated
            .anyRequest().authenticated()
              .and()
              .formLogin()
              .loginPage("/auth/loginPage");
    }
    
    // Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
    // The UserDetailsService object is used by the auth manager to load the user from database.
    // In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
    
    @Bean
    public JwtConfig jwtConfig() {
        return new JwtConfig();
    }
}

auth -> UsernamePasswordAuthenticationFilter

public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    
        private AuthenticationManager authManager;
        
        private final JwtConfig jwtConfig;
        
        public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) {
            this.authManager = authManager;
            this.jwtConfig = jwtConfig;
            
            // By default, UsernamePasswordAuthenticationFilter listens to "/login" path. 
            // In our case, we use "/auth". So, we need to override the defaults.
            //this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));

            this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
                    new AntPathRequestMatcher("/auth/**")
                    , new AntPathRequestMatcher("/user/register")
            ));
        }
        
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
            
            try {
                
                // 1. Get credentials from request
                UserDTO creds = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
                
                // 2. Create auth object (contains credentials) which will be used by auth manager
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        creds.getUsername(), creds.getPassword(), Collections.emptyList());
                
                // 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
                return authManager.authenticate(authToken);
                
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        
        // Upon successful authentication, generate a token.
        // The 'auth' passed to successfulAuthentication() is the current authenticated user.
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                Authentication auth) throws IOException, ServletException {
            Long now = System.currentTimeMillis();
            String token = Jwts.builder()
                .setSubject(auth.getName()) 
                // Convert to list of strings. 
                // This is important because it affects the way we get them back in the Gateway.
                .claim("authorities", auth.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
                .setIssuedAt(new Date(now))
                .setExpiration(new Date(now + jwtConfig.getExpiration() * 1000))  // in milliseconds
                .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
                .compact();
            
            // Add token to header
            response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
        }
}

Controller

@GetMapping("/auth/loginPage")
    public String loginPage() {
        return "login";
    }
3 Answers

I think your problem is here in JwtUsernameAndPasswordAuthenticationFilter

You also have this point commented out. You are triggering this filter on POST and GET. You only want to trigger it for POST.

Current method

            this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
                    new AntPathRequestMatcher("/auth/**")
                    , new AntPathRequestMatcher("/user/register")
            ));

Updated

            this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
                    new AntPathRequestMatcher("/auth/**", "POST")
                    , new AntPathRequestMatcher("/user/register", "POST")
            ));

By doing this:

    this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(                                                                                     
            new AntPathRequestMatcher("/auth/**")
            , new AntPathRequestMatcher("/user/register")
    ));

the filter will authenticate any request to /auth/** (thus /auth/loginPage) and because you set your authentication entry point to just return 401 status you will have that issue.

just comment this:

.and()
    // handle an authorized attempts 
    .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))  

and it should redirect you to the login page.

PS: Based on your configuration if I'm not authenticated and trying to access /auth/loginPage I'll be redirected to /auth/LoginPage, and once I enter the creds I'll be authenticated successfully and redirected again to the same page /auth/loginPage

How can i ignore authentication for login page.

OncePerRequestFilter has a method shouldNotFilter that you can override.

For example:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return new AntPathMatcher().match("/auth/loginPage", request.getServletPath());
}
Related