Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing JWT Authentication on Spring Boot APIs

I have a SpringBoot 2.0.2.RELEASE web application, with this config file:

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

    final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
    if (activeProfiles.contains("dev")) {
        http.csrf().disable();
        http.headers().frameOptions().disable();
    }

    http
        .authorizeRequests()
        .antMatchers(publicMatchers()).permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin().loginPage("/login").defaultSuccessUrl("/bonanza/list")
        .failureUrl("/login?error").permitAll()
        .and()
        .logout().permitAll();
    }

I want to add a Custom JWT based security filter ONLY for the Rest Controllers that will be under the match /rest/** , so I modified the config to this file, but now I can't log into the app, because I have a HTTP Status 401 – Unauthorized

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

    final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
    if (activeProfiles.contains("dev")) {
        http.csrf().disable();
        http.headers().frameOptions().disable();
    }

    http
       .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
       // don't create session
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
       .authorizeRequests()
       .antMatchers(publicMatchers()).permitAll()
       .anyRequest().authenticated()
       .and()
       .formLogin().loginPage("/login").defaultSuccessUrl("/bonanza/list")
                   .failureUrl("/login?error").permitAll()
       .and()
       .logout().permitAll();


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

and the filter (that extends from OncePerRequestFilter )

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

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


    if (request.getRequestURI().indexOf("/rest/")==-1) {
        chain.doFilter(request, response);
        return;
    }


    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");
    }

    logger.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));
            logger.info("authorizated user '{}', setting security context", username);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(request, response);
    }

....

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return request.getRequestURI().indexOf("/rest/")==-1;
}

in the logger I see

("couldn't find bearer string, will ignore the header"

Because I only want to apply the JWT filter in the RestContollers not in all of them, like LoginController

With this config class I can access to the /rest/ URL only being logged in the app.

@Profile("web")
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Logger LOG = LoggerFactory.getLogger(WebSecurityConfig.class);

    @Autowired
    private UserSecurityService userSecurityService;


    @Value("${server.servlet.context-path}")
    private String serverContextPath;

    /** The encryption SALT. */
    private static final String SALT = "fd&lkj§sfs23#$1*(_)nof";

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

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

    @Configuration
    @Order(1)
    public static class ApiSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private JwtAuthenticationEntryPoint unauthorizedHandler;

        @Autowired
        private JwtTokenUtil jwtTokenUtil;

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

        @Value("${jwt.route.authentication.path}")
        private String authenticationPath;

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

            http
                    // 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()
                    .antMatchers(“/rest/**”).permitAll().anyRequest().authenticated()
                    .antMatchers(“**/rest/**”).permitAll().anyRequest().authenticated();

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

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

        }

    }

    @Configuration
    @Order(0)
    public static class OtherSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Value("${server.servlet.context-path}")
        private String serverContextPath;

        @Autowired
        private Environment env;

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

            final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
            if (activeProfiles.contains("dev")) {
                http.csrf().disable();
                http.headers().frameOptions().disable();
            }

            http.authorizeRequests()
                .antMatchers(publicMatchers())
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/bonanza/list")
                .failureUrl("/login?error").permitAll()
                .and()
                .logout()
                .permitAll();
        }

         private String[] publicMatchers() {

             /** Public URLs. */
            final String[] PUBLIC_MATCHERS = {
                    "/webjars/**",
                    serverContextPath + "/css/**",
                    serverContextPath + "/js/**",
                    serverContextPath + "/fonts/**",
                    serverContextPath + "/images/**",                
                    serverContextPath ,
                    "/",
                    "/error/**/*",
                    "/console/**",
                    ForgotMyPasswordController.FORGOT_PASSWORD_URL_MAPPING,
                    ForgotMyPasswordController.CHANGE_PASSWORD_PATH,
                    SignupController.SIGNUP_URL_MAPPING
            };

            return PUBLIC_MATCHERS;

        }

    }

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

}
like image 211
Nunyet de Can Calçada Avatar asked May 17 '18 07:05

Nunyet de Can Calçada


People also ask

What is JWT implement it using spring boot security?

JSON Web Token or JWT, as it is more commonly called, is an open Internet standard (RFC 7519) for securely transmitting trusted information between parties in a compact way. The tokens contain claims that are encoded as a JSON object and are digitally signed using a private secret or a public key/private key pair.


1 Answers

In a nutshell, you have two subpaths (namely /rest/** and others) in the same application, and you want to apply different login schemes for each. Spring-security allows you to have multiple configurations, allowing for such a scenario.

I would do something like this:

@EnableWebSecurity
public class SecurityConfig {

    @Configuration
    @Order(1)
    public static class ApiSecurityConfiguration 
                  extends WebSecurityConfigurerAdapter {

        private final JwtAuthorizationTokenFilter jwtFilter = new ...
        private final AuthenticationEntryPoint unauthorizedHandler = new ...

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/rest/**").authorizeRequests()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                .addFilter(jwtFilter);
        }
    }

    @Configuration
    public static class OtherSecurityConfiguration 
                  extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                        .loginPage("/login").defaultSuccessUrl("/bonanza/list")
                        .failureUrl("/login?error").permitAll()
                    .and()
                    .logout().permitAll();
        }
    }
}

With such configuration, the JwtAuthorizationTokenFilter should only be activated for the matching paths. Thus I think you won't need to check for the paths in JwtAuthorizationTokenFilter.

like image 146
MuratOzkan Avatar answered Sep 28 '22 06:09

MuratOzkan