-1

I have read How to apply spring security filter only on secured endpoints?, which seems to be the closest to my question, but does not sufficiently answer it.

Further below you will see a WebSecurityConfigurerAdapter-configuration I am currently using. It will not remain like this as I will not expose h2-console later on.

My problem is, that JwtAuthenticationFilter is always executed. I'd rather want the filter to be executed on requests, which demand authentication (in my particular case: only what's described here:

.authorizeRequests()
                .anyRequest()
                    .authenticated()

).

How to achieve this?

P.s.: my application login works as expected while H2-console does, too, but keeps throwing io.jsonwebtoken.SignatureException, because the JWT H2-console generates and uses is naturally different from the one my application uses.

WebSecurityConfigurerAdapter:

package com.particles.authservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.particles.authservice.jwt.JwtAuthenticationEntryPoint;
import com.particles.authservice.jwt.JwtAuthenticationFilter;
import com.particles.authservice.service.UserService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Override
    public void configure(final AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                                    .userDetailsService(userService)
                                    .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        //@formatter:off
        http
            .cors()
                .and()
            .csrf()
                .disable()
            .headers()
                .frameOptions()
                    .disable()
                    .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                    .permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST, "/register")
                    .permitAll()
                .antMatchers(HttpMethod.GET, "/confirm")
                    .permitAll()
                .antMatchers(HttpMethod.POST, "/login")
                    .permitAll()
                .antMatchers(HttpMethod.GET, "/user")
                    .permitAll()
                .and()
            .authorizeRequests()
                .anyRequest()
                    .authenticated()
                .and()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .and()
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            ;
        //@formatter:on
    }

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

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Edit: here's the JwtAuthenticationFilter. If you need the TOs as well, let me know.

JwtAuthenticationFilter:

package com.particles.authservice.jwt;

import java.io.IOException;
import java.util.Optional;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.particles.authservice.tos.UserJwt;

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private static final String AUTHORIZATION_HEADER_PREFIX               = "Authorization";
    private static final String AUTHORIZATION_HEADER_BEARER_PREFIX        = "Bearer ";
    private static final int    AUTHORIZATION_HEADER_BEARER_PREFIX_LENGTH = AUTHORIZATION_HEADER_BEARER_PREFIX.length();

    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getHeader(AUTHORIZATION_HEADER_PREFIX) != null) {
            final Optional<String> optToken = extractTokenFromRequest(request);

            if (optToken.isPresent() && StringUtils.hasText(optToken.get()) && jwtService.isTokenValid(optToken.get())) {
                // if token exists and is valid, retrieve corresponding UserJwt-object
                final UserJwt jwt = jwtService.getJwtFromToken(optToken.get());

                final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwt.getUser(), null,
                        jwt.getUser().getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request, response);
    }

    /**
     * This method extracts a JWT from a {@link HttpServletRequest}-object.
     *
     * @param request
     *            ({@link HttpServletRequest}) request, which supposedly contains a JWT
     * @return (Optional&lt;String&gt;) JWT as String
     */
    private Optional<String> extractTokenFromRequest(final HttpServletRequest request) {
        final String bearerToken = request.getHeader(AUTHORIZATION_HEADER_PREFIX);

        String bearerTokenContent = null;
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(AUTHORIZATION_HEADER_BEARER_PREFIX)) {
            bearerTokenContent = bearerToken.substring(AUTHORIZATION_HEADER_BEARER_PREFIX_LENGTH, bearerToken.length());
        }

        return Optional.ofNullable(bearerTokenContent);
    }
}

If you need to see any other classes, tell me and I will paste them here.

Igor
  • 1,582
  • 6
  • 19
  • 49
  • @Code_Is_Law: I added the JwtAuthenticationFilter source code. `UserJwt` class consists of 3 attributes: String token, PersistableUser user (entity containing user data and implementing UserDetails) and Date expiresOn. UserJwt is an entity as well, because I decided to save tokens in order to allow a proper logout. – Igor Feb 23 '20 at 11:58

2 Answers2

0

Add the below method to expose your public endpoints

 @Override
 public void configure(WebSecurity web) throws Exception {
    web.ignoring()
    .antMatchers("/public-api/**");
  }
Thirumal
  • 8,280
  • 11
  • 53
  • 103
  • I'd totally do this, but currently my API is under `localhost:9191/auth/*` and so is the h2-console (`localhost:9191/auth/h2-console`). Should I try to readjust to having all my public endpoints under `localhost:9191/auth/publicapi` while the private API is exposed under `localhost:9191/auth/privateapi` and while the H2-console is exposed at `localhost:9191/auth/h2-console`? – Igor Feb 23 '20 at 11:51
0

It does not seem possible to only apply the filter to specific endpoints using solely the WebSecurityConfigurerAdapter#configure method.

Instead I decided to separate the endpoints in private-API and all the other endpoints.

  • endpoints, which need the filter (in my case JwtAuthenticationFilter), are collected under /api/ and not defined individually, because chances are high someone forgets to add them to the configure-method
  • all the other endpoints have a different path than /api/

My configure-method goes like this:

@Override
    protected void configure(final HttpSecurity http) throws Exception {
        //@formatter:off
        http
            .cors()
                .and()
            .csrf()
                .disable()
            .headers()
                .frameOptions()
                    .disable()
                    .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                    .permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST,
                        PUBLIC_API_PATH + "register",
                        PUBLIC_API_PATH + "login")
                    .permitAll()
                .antMatchers(HttpMethod.GET,
                        PUBLIC_API_PATH + "confirm")
                    .permitAll()
                .and()
            .authorizeRequests()
                .anyRequest()
                    .authenticated()
                .and()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .accessDeniedHandler(unauthorizedHandler)
                .and()
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            ;
        //@formatter:on

In JwtAuthenticationFilter I check whether the request-path contains the private-API path /api/. I only apply the filtering if it does.

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getRequestURI().contains(SecurityConfiguration.PRIVATE_API_PATH)) {
            // perform Jwt-authentication since request-URI suggests a call to private-API
...
            }
        }

        filterChain.doFilter(request, response);
    }

I do not like this solution, especially because I have to keep SecurityConfiguration.PRIVATE_API_PATH a constant since @*Mapping(value) expects a constant. It gets the job done, though.

If you have a better suggestion, I am eager to try it out.

Edit Apparently it is possible to use variables in @*Mapping(value) like so: @PostMapping(value = "${apipath}/user"). So I can make the path configurable after all - but the check in JwtAuthenticationFilter must remain nevertheless; I just do not have to use constants, but variables, which contain values from e.g. application.yaml.

Igor
  • 1,582
  • 6
  • 19
  • 49