4
votes

I have a SpringBoot 2.4.2 application that uses JSON Web Tokens (JWT, sometimes pronounced /dʒɒt/, the same as the English word "jot"[1]) is an Internet proposed standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims. The tokens are signed either using a private secret or a public/private key. For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that it is logged in as admin.

This is my WebSecurityConfig:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String SALT = "fd23451*(_)nof";

    private final JwtAuthenticationEntryPoint unauthorizedHandler;
    private final JwtTokenUtil jwtTokenUtil;
    private final UserSecurityService userSecurityService;

    @Value("${jwt.header}")
    private String tokenHeader;


    public ApiWebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, JwtTokenUtil jwtTokenUtil,
            UserSecurityService userSecurityService) {
        this.unauthorizedHandler = unauthorizedHandler;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userSecurityService = userSecurityService;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userSecurityService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));
    }

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

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

        httpSecurity
                // we don't need CSRF because our token is invulnerable
                .csrf().disable()

                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

                // don't create session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // Un-secure H2 Database
                .antMatchers("/h2-console/**/**").permitAll()
                .antMatchers("/api/v1/users").permitAll()
                .anyRequest().authenticated();

        // Custom JWT based security filter
        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
        httpSecurity
                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // disable page caching
        httpSecurity
                .headers()
                .frameOptions()
                .sameOrigin()  // required to set for H2 else H2 Console will be blank.
                .cacheControl();
    }

    @Override
    public void configure(WebSecurity web) {

        // AuthenticationTokenFilter will ignore the below paths
        web
                .ignoring()
                .antMatchers(
                        HttpMethod.POST,
                        "/api/v1/users"
                );

    }

}

and this is my Filter:

@Provider
@Slf4j
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;
    private String tokenHeader;

    public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.tokenHeader = tokenHeader;
    }


    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return new AntPathMatcher().match("/api/v1/users", request.getServletPath());
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException,
            IOException {

        log.info("processing authentication for '{}'", request.getRequestURL());

        final String requestHeader = request.getHeader(this.tokenHeader);

        String username = null;
        String authToken = null;

        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            authToken = requestHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(authToken);
            } catch (IllegalArgumentException e) {
                logger.info("an error occured during getting username from token", e);
            } catch (ExpiredJwtException e) {
                logger.info("the token is expired and not valid anymore", e);
            }
        } else {
            logger.info("couldn't find bearer string, will ignore the header");
        }

        log.info("checking authentication for user '{}'", username);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            logger.info("security context was null, so authorizating user");

            // It is not compelling necessary to load the use details from the database. You could also store the information
            // in the token and read it from it. It's up to you ;)
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // For simple validation it is completely sufficient to just check the token integrity. You don't have to call
            // the database compellingly. Again it's up to you ;)
            if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                log.info("authorizated user '{}', setting security context", username);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

and

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException) throws IOException {

        log.info("user tries to access a secured REST resource without supplying any credentials");

        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

This is the console when I start the app:

18:02:51.974 [restartedMain] DEBUG com.agrumh.Application - Running with Spring Boot v2.4.2, Spring v5.3.3
18:02:51.974 [restartedMain] INFO  com.agrumh.Application - No active profile set, falling back to default profiles: default
18:02:57.383 [restartedMain] INFO  o.s.s.web.DefaultSecurityFilterChain - Will secure Ant [pattern='/api/v1/users', POST] with []
18:02:57.414 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/h2-console/**/**']
18:02:57.415 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/api/v1/users']
18:02:57.416 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [authenticated] for any request
18:02:57.422 [restartedMain] INFO  o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@24c68fed, org.springframework.security.web.context.SecurityContextPersistenceFilter@1537eb0a, org.springframework.security.web.header.HeaderWriterFilter@95de45c, org.springframework.security.web.authentication.logout.LogoutFilter@733cf550, com.dispacks.config.JwtAuthorizationTokenFilter@538a96c8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@8d585b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@784cf061, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@64915f19, org.springframework.security.web.session.SessionManagementFilter@21f180d0, org.springframework.security.web.access.ExceptionTranslationFilter@2b153a28, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4942d157]
18:02:58.619 [restartedMain] INFO  com.dispacks.DispacksApplication - Started DispacksApplication in 6.974 seconds (JVM running for 7.697)
18:04:03.685 [http-nio-1133-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing POST /error
18:04:03.687 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
18:04:03.689 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]
18:04:03.698 [http-nio-1133-exec-1] INFO  c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials
18:04:03.699 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request

But when I access with Postman I have this error:

22:58:33.562 [http-nio-1133-exec-2] WARN  o.s.w.s.m.s.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported]
22:58:33.579 [http-nio-1133-exec-2] INFO  c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials
3
Did you solve the problem? If you breakpointed the exception, what did the exception message say? - justthink

3 Answers

0
votes

Authorization and authentication are different The POST /api/v1/users was allowed, because the resource POST does not need to be authorized to be accessed.

In your code,

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException // AuthenticationException means authentication failed, not "without supplying any credentials".
    ) throws IOException {

// Break point here, or print authException.

        log.info("user tries to access a secured REST resource without supplying any credentials"); // Wrong message. You can say "Authentication failed.", or log.info(authException.getMessage()).

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }

The authentication error actually happens when accessing /error resource.

18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]

I assume some error happened, your application is redirecting you to /error, but the /error is protected. So authenticationException happened on /error.

  1. Add /error before .permitAll().
  2. Breakpoint the authenticationException so I can update this answer.
0
votes

What is the path that you call from Postman? If it's /api/v1/users I can see that you have this path set in the shouldNotFilter method of your filter. Doesn't that mean that you're ignoring your JWT filter for this path?

By the way, if you don't need any additional functionality you can use Spring Security's support for validating JWTs. Have a look at this tutorial to see how it's configured. This way you will not need your own filter.

0
votes

If i understand you correct, you want the JWT-filter to run only for certain endpoints? I had this same problem that I couldn't get SpringSecurity to only run my JWT-filter for specified entrypoints no matter how much I tried diffrent security configs.

I solved this by overriding shouldNotFilter as you did, but mine looks something like this:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return new AntPathRequestMatcher("/api/v1/users").matches(request);
}

Perhaps this could solve your problem.