Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security MultiHttpSecurity Configuration so that I can perform two types of authentication. JWT Token and Session Cookie

I already have Spring Security Cookie mechanism in place for my application, now only for the API's, I need to add JWT Token-based authentication mechanism. I'm using Spring Security's MultiHttpSecurityConfiguration with two nested class.

Whether both session and JWT token mechanism should be included together in one application or not is a different question altogether, I need to achieve two things.

  1. Spring Security's session-based authentication with cookie will work as it was before.
  2. Need to add an authentication header for API's
package com.leadwinner.sms.config;

import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import com.leadwinner.sms.CustomAuthenticationSuccessHandler;
import com.leadwinner.sms.CustomLogoutSuccessHandler;
import com.leadwinner.sms.config.jwt.JwtAuthenticationProvider;
import com.leadwinner.sms.config.jwt.JwtAuthenticationTokenFilter;
import com.leadwinner.sms.config.jwt.JwtSuccessHandler;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@ComponentScan(basePackages = "com.leadwinner.sms")
public class MultiHttpSecurityConfig {

    @Autowired
    @Qualifier("userServiceImpl")
    private UserDetailsService userServiceImpl;

    @Autowired
    private JwtAuthenticationProvider authenticationProvider;

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

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

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(Collections.singletonList(authenticationProvider));
    }

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

         @Autowired
         private JwtAuthenticationTokenFilter jwtauthFilter;

        @Override
        public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .antMatcher("/web/umgmt/**").authorizeRequests()
            .antMatchers("/web/umgmt/**").authenticated()
            .and()
            .addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);
         http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }

    @Configuration
    @Order(2)
    public static class SecurityConfig extends WebSecurityConfigurerAdapter {
        private  final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

        @Bean
        public CustomAuthenticationEntryPoint getBasicAuthEntryPoint() {
            return new CustomAuthenticationEntryPoint();
        }

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

            logger.info("http configure");
            http
            .antMatcher("/**").authorizeRequests()          
            .antMatchers("/login/authenticate").permitAll()
                    .antMatchers("/resources/js/**").permitAll()
                    .antMatchers("/resources/css/**").permitAll()
                    .antMatchers("/resources/images/**").permitAll()
                    .antMatchers("/web/initial/setup/**").permitAll()
                    .antMatchers("/dsinput/**").permitAll().antMatchers("/dsoutput/**").permitAll()                 

                    .and()
                .formLogin()
                    .loginPage("/login").usernameParameter("employeeId").passwordParameter("password")
                    .successForwardUrl("/dashboard")
                    .defaultSuccessUrl("/dashboard", true)
                    .successHandler(customAuthenticationSuccessHandler())
                    .failureForwardUrl("/logout")
                    .loginProcessingUrl("/j_spring_security_check")
                    .and().logout()
                    .logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
                    .logoutSuccessHandler(customLogoutSuccessHandler())
                    .permitAll()
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID")
                    .and().sessionManagement()
                    .sessionFixation().none()
                    .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                    .invalidSessionUrl("/logout")
                    .and().exceptionHandling().accessDeniedPage("/logout").and().csrf().disable();
            http.authorizeRequests().anyRequest().authenticated();


        }

        @Bean
        public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
            return new CustomAuthenticationSuccessHandler();
        }

        @Bean
        public LogoutSuccessHandler customLogoutSuccessHandler() {
            return new CustomLogoutSuccessHandler();
        }
    }
}

JwtAuthenticationTokenFilter.java

package com.leadwinner.sms.config.jwt;

import java.io.IOException;

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.web.filter.OncePerRequestFilter;

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        final String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String authToken = header.substring(7);
            System.out.println(authToken);

            try {
                String username = jwtTokenUtil.getUsernameFromToken(authToken);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    if (jwtTokenUtil.validateToken(authToken, username)) {
                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                                username, null, null);
                        usernamePasswordAuthenticationToken
                                .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                    }
                }
            } catch (Exception e) {
                System.out.println("Unable to get JWT Token, possibly expired");
            }
        }

        chain.doFilter(request, response);
    }
}

JwtTokenUtil.java

package com.leadwinner.sms.config.jwt;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Component
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = 8544329907338151549L;
    public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    private String secret = "my-secret";

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, username);
    }

    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return "Bearer "
                + Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                        .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                        .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    public Boolean validateToken(String token, String usernameFromToken) {
        final String username = getUsernameFromToken(token);
        return (username.equals(usernameFromToken) && !isTokenExpired(token));
    }
}

It seems that now the JwtSecurityConfig filter is not being applied for the path I have mentioned. Any help will be appreciated.

I have already read this question. I followed the same.

Spring Security with Spring Boot: Mix Basic Authentication with JWT token authentication

Edit: Added JwtAuthenticationTokenFilter, JwtTokenUtil

like image 399
Shiva kumar Avatar asked Oct 04 '19 06:10

Shiva kumar


People also ask

How does JWT authentication works in Spring boot?

In the JWT auth process, the front end (client) firstly sends some credentials to authenticate itself (username and password in our case, since we're working on a web application). The server (the Spring app in our case) then checks those credentials, and if they are valid, it generates a JWT and returns it.


1 Answers

I got your requirement.

  1. You need to expose API's that should be accessed through JWT token in request header(for each request).
  2. And also web application should be secured through form based authentication mechanism which should work on basis of http session.

You can achieve this by two authentication filters.

Filter - 1: for Rest API (JwtAuthTokenFilter) which should be stateless and identified by Authorization token sent in request each time.
Filter - 2: You need another filter (UsernamePasswordAuthenticationFilter) By default spring-security provides this if you configure it by http.formLogin(). Here each request is identified by the session(JSESSIONID cookie) associated. If request does not contain valid session then it will be redirected to authentication-entry-point (say: login-page).

Recommended URL pattern
api-url-pattern    = "/api/**" [strictly for @order(1)]
webApp-url-pattern = "/**" [ wild card "/**" always used for higer order otherwise next order configuration becomes dead configuration]

Approach

  • Define Main Configuration class with @EnableWebSecurity

  • Create two inner static classes which should extend WebSecurityConfigurerAdapter and annotated with @Configuration and @Order. Here order for rest api configuration should be 1 and for web application configuration order should be more than 1

  • Refer my answer in this link for more details which has explaination in depth with necessary code. Feel free to ask for downloadable link from github repository if required.

Limitation
Here both filters will work side by side(Parellally). I mean from web application even though if a user is authenticated by session, he can not access API's without a JWT token.

EDIT
For OP's requirement where he doesn't want to define any role but API access is allowed for authenticated user. For his requirement modified below configuration.

http.csrf().disable()
.antMatcher("/web/umgmt/**").authorizeRequests()
.antMatcher("/web/umgmt/**").authenticated() // use this
like image 151
PraveenKumar Lalasangi Avatar answered Oct 23 '22 06:10

PraveenKumar Lalasangi