Skip to content

JWT Token In Spring Security#

Spring Security Project Configuration#

  • In this session we will try to apply JWT into our spring security application for both Authentication and Authorization through custom filters. In real projects, there are many other ways for handling JWT in spring security and we will try it for next sessions.
  • Before implementing the JWT token in our spring security application, we need to import some dependencies and add configurations in following sections.

Dependencies#

  • We will need to import dependencies below in the pom.xml for JWT token impelementation.
pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
    </dependency>

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.4</version>
    </dependency>

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.6.4</version>
    </dependency>

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.6.6</version>
    </dependency>

    <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
    </dependency>

    <!--JWT TOKEN-->
    <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
    </dependency>

    <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
    </dependency>

    <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
    </dependency>

Http Security Configuration#

  • As we have already known about the Session token that spring security will generated automatically by default in Spring Security Basic. So with this default behavior, a user will be authenticated for the first time he sends the request and received a session token (JSESSIONID) in the response cookies. Then for next times, the user just need to send the request with the JSESSIONID then he will be authenticated automatically without checking credentials.
  • Our scope is applying JWT token into our spring security application so we need to disable this default generating JSESSIONID. So we need to add some configurations as below into the ProjectSecurityConfig class.
ProjectSecurityConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.spring.security.spring.security.jwt.config;

import com.spring.security.spring.security.jwt.constant.SecurityConstant;
import com.spring.security.spring.security.jwt.filter.AuthoritiesLoggingAfterFilter;
import com.spring.security.spring.security.jwt.filter.AuthoritiesLoggingAtFilter;
import com.spring.security.spring.security.jwt.filter.RequestValidationBeforeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;

@Configuration
public class ProjectSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthoritiesLoggingAfterFilter authoritiesLoggingAfterFilter;
    @Autowired
    private AuthoritiesLoggingAtFilter authoritiesLoggingAtFilter;
    @Autowired
    private RequestValidationBeforeFilter requestValidationBeforeFilter;

    /**
     *
     * contact: Not Secure
     * notice: Not Secure
     * balance: Secure
     * Card: Secure
     * Loan: Secure
     * Account: Secure
     *
     */
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().csrf()
                .ignoringAntMatchers("/v1/user")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and().cors()
                .and().addFilterBefore(requestValidationBeforeFilter, BasicAuthenticationFilter.class)
                .addFilterAfter(authoritiesLoggingAfterFilter, BasicAuthenticationFilter.class)
                .addFilterAt(authoritiesLoggingAtFilter, BasicAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/v1/user").hasAnyRole("USER", "ADMIN")
                .antMatchers("/v1/accounts/**").hasRole("USER")
                .antMatchers("/v1/balance").hasRole("USER")
                .antMatchers("/v1/loan").hasRole("ADMIN")
                .antMatchers("/v1/card").hasRole("ADMIN")
                .antMatchers("/v1/contact").permitAll()
                .antMatchers("/v1/notice").permitAll()
                .and().formLogin()
                .and().httpBasic();
    }

    @Bean
    protected CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setExposedHeaders(Collections.singletonList(SecurityConstant.AUTHORIZATION_HEADER));
        corsConfiguration.setMaxAge(3600L);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

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

}
  • When you look into the configuration class above, you will see we will disable generating JSESSIONID automatically by setting http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) in which SessionCreationPolicy.STATELESS means Spring Security will never create a HttpSession and it will never use it to obtain the SecurityContext. That means when we generate the jwt token we will handle and mange it by ourself.
  • Then in the CORS configuration we will add corsConfiguration.setExposedHeaders(Collections.singletonList("Authorization")); to expose the response header Authorization for client. Because after generating a JWT token we will set it into this response header.

Create Filters For Jwt Tokens#

  • Now, we will create two filters in which one filter is used for creating jwt Token and the other is used for validate the jwt token.
  • Before creating filters we will need to create contain class as below:
SecurityConstant.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.spring.security.spring.security.jwt.constant;

public class SecurityConstant {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";
    public static final String CLAIM_USERNAME = "username";
    public static final String CLAIM_AUTHORITIES = "authorities";
    public static final String ISSUER = "2D Garden";
    public static final String SUBJECT = "JWT";

}

Filter For Creating Jwt Token#

  • We will create a filter name JwtTokenGeneratorFilter which will extend the OncePerRequestFilter abstract class because we want to make the token is just created only one time when our application receive a request which has not authenticated yet. The implementation code will look like below.
JwtTokenGeneratorFilter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.spring.security.spring.security.jwt.filter;

import com.spring.security.spring.security.jwt.constant.SecurityConstant;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.SecretKey;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Component
public class JwtTokenGeneratorFilter extends OncePerRequestFilter {

    @Value("${security.token.secret}")
    private String secret;
    @Value("${security.token.timeout}")
    private Long timeout;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (Objects.nonNull(authentication)) {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
            Date now = new Date();
            String jwt = Jwts.builder()
                    .setIssuer(SecurityConstant.ISSUER)
                    .setSubject(SecurityConstant.SUBJECT)
                    .claim(SecurityConstant.CLAIM_USERNAME, authentication.getName())
                    .claim(SecurityConstant.CLAIM_AUTHORITIES, this.getAuthorityString(authentication.getAuthorities()))
                    .setIssuedAt(now)
                    .setExpiration(new Date(now.getTime() + timeout))
                    .signWith(key)
                    .compact();
            response.setHeader(SecurityConstant.AUTHORIZATION_HEADER, jwt);
        }
        filterChain.doFilter(request, response);
    }

    private String getAuthorityString(Collection<? extends GrantedAuthority> grantedAuthorities) {
        Set<String> authorities = new HashSet<>();
        grantedAuthorities.forEach(g -> authorities.add(g.getAuthority()));
        return String.join(",", authorities);
    }
}
  • So as you can see in the doFilterInternal(), we will check the SecurityContext that contains the Authentication or not. If SecurityContext had contained the Authentication so it means the user had been logged in successfully and we will generate the Jwt token and put it in the response's header for the user. Then we will use the Jwts.builder() to build the Jwt Token, we also need a Secret key which is a random string that we put in the application.yml.
application.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/worldbank?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: password
  jpa:
    hibernate.ddl-auto: update
    hibernate.dialect: org.hibernate.dialect.MySQL5Dialect
    generate-ddl: true
    show-sql: true

security:
  token:
    secret: 2472B08201704D96A5844807E43BA02A9E0CCA945381123A82B1DD4E852B9325
    timeout: 30000

logging:
  level:
    org.springframework.security.web.FilterChainProxy: DEBUG
  • Then in the Jwt Body token we will put some information like: Issuer, Subject, username claim, authorities claim, IssuedAt and Expiration.
Field Set Method Value
iss setIssuer() hardcoded
sub setSubject() hardcoded
username claim() value is got from Authentication
authorities claim() value is got from Authentication
iat setIssuedAt() currentDate
exp setExpiration() currentDate + 30s
  • The life time of a jwt token will based on the issuedAt and expiration. In which, the issueAt is the time that the jwt is created and expiration is the time that the jwt will be expired and can't use anymore. In this example the jwt token life time will be 30s after it is created.

Filter For Validating Jwt Token#

  • We will also create a filter name JwtTokenValidatorFilter which will extend the OncePerRequestFilter abstract class because we want to make the token is just validated only one time when our application receive a request. The implementation code will look like below.
JwtTokenValidatorFilter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.spring.security.spring.security.jwt.filter;

import com.spring.security.spring.security.jwt.constant.SecurityConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.SecretKey;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

@Component
public class JwtTokenValidatorFilter extends OncePerRequestFilter {

    @Value("${security.token.secret}")
    private String secret;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String bearerToken = request.getHeader(SecurityConstant.AUTHORIZATION_HEADER);
        if (this.isBearerToken(bearerToken)) {
            try {
                String jwt = bearerToken.substring(SecurityConstant.BEARER_PREFIX.length());
                SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
                Claims claims = Jwts.parserBuilder()
                        .setSigningKey(key)
                        .build()
                        .parseClaimsJws(jwt)
                        .getBody();
                String username = String.valueOf(claims.get(SecurityConstant.CLAIM_USERNAME));
                String authorities = (String) claims.get(SecurityConstant.CLAIM_AUTHORITIES);
                Authentication authentication = new UsernamePasswordAuthenticationToken(username, null,
                        AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (Exception e) {
                throw new BadCredentialsException("Invalid Token!");
            }
        }
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return request.getServletPath().equals("/v1/user");
    }

    private boolean isBearerToken(String token) {
        return Objects.nonNull(token) && token.length() > 7 && token.startsWith(SecurityConstant.BEARER_PREFIX);
    }
}
  • So as you can see in the doFilterInternal(), we will try to get the Authorization header in the request, then we will check the content of this header is the Bearer token or not, if it is the Bearer token so we will extract the jwt token.
  • After we got the jwt token, we will rebuild the Secret key that we used before in JwtTokenGeneratorFilter. We will parse the jwt with the key and get the body by the method parseClaimsJws(<jwt token>).getBody(). So in the parseClaimsJws the signature will be checked to make the jwt token is not modified after it is created and transferred from client to our spring security application. If there is no any SignatureException() had thrown, so the jwt is valid and we can get parameters from Claims like username and authorities that we set before in JwtTokenGeneratorFilter.
  • Finally, from information like username and authorities, we will create an Authentication and set it into the SecurityContext.
  • Moreover, you can also see in JwtTokenValidatorFilter, we will also implement the method shouldNotFilter() for the api /v1/user which is used for login user. It means that any request come into this api will be ignored by JwtTokenValidatorFilter because we want the user should be able to login into our application.

Add Jwt Filters Into Filter Chain#

  • After creating 2 filters: JwtTokenGeneratorFilter and JwtTokenValidatorFilter, we will add the JwtTokenGeneratorFilter after the BasicAuthenticationFilter because we want to make sure users had to log in successfully before we create jwt token for them in the responses. Then the filter JwtTokenValidatorFilter will be added before BasicAuthenticationFilter because we want to make any request with contains Authorization header with jwt token will be validated and used for authentication and authorization.
ProjectSecurityConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.spring.security.spring.security.jwt.config;

import com.spring.security.spring.security.jwt.constant.SecurityConstant;
import com.spring.security.spring.security.jwt.filter.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;

@Configuration
public class ProjectSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthoritiesLoggingAfterFilter authoritiesLoggingAfterFilter;
    @Autowired
    private AuthoritiesLoggingAtFilter authoritiesLoggingAtFilter;
    @Autowired
    private RequestValidationBeforeFilter requestValidationBeforeFilter;
    @Autowired
    private JwtTokenGeneratorFilter jwtTokenGeneratorFilter;
    @Autowired
    private JwtTokenValidatorFilter jwtTokenValidatorFilter;

    /**
     *
     * contact: Not Secure
     * notice: Not Secure
     * balance: Secure
     * Card: Secure
     * Loan: Secure
     * Account: Secure
     *
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().csrf()
                .ignoringAntMatchers("/v1/user")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and().cors()
                .and().addFilterBefore(requestValidationBeforeFilter, BasicAuthenticationFilter.class)
                .addFilterAfter(authoritiesLoggingAfterFilter, BasicAuthenticationFilter.class)
                .addFilterBefore(jwtTokenValidatorFilter, BasicAuthenticationFilter.class)
                .addFilterAfter(jwtTokenGeneratorFilter, BasicAuthenticationFilter.class)
                .addFilterAt(authoritiesLoggingAtFilter, BasicAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/v1/user").hasAnyRole("USER", "ADMIN")
                .antMatchers("/v1/accounts/**").hasRole("USER")
                .antMatchers("/v1/balance").hasRole("USER")
                .antMatchers("/v1/loan").hasRole("ADMIN")
                .antMatchers("/v1/card").hasRole("ADMIN")
                .antMatchers("/v1/contact").permitAll()
                .antMatchers("/v1/notice").permitAll()
                .and().formLogin()
                .and().httpBasic();
    }

    @Bean
    protected CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setExposedHeaders(Collections.singletonList(SecurityConstant.AUTHORIZATION_HEADER));
        corsConfiguration.setMaxAge(3600L);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

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

}

Testing#

  • So now, let's start our spring security application and call the api /v1/user to login and get the jwt token in the response header.

 #zoom

  • Then, let's copy this jwt token and add it into the Authorization request with Bearer Token type of another api /v1/loan and call it. You should see the response 200 as below.

 #zoom

  • Then if you look into the response header of /v1/load, you can also see there is another jwt response and you can also use this jwt token to call to our applications.

 #zoom

  • Now, if you do nothing and wait for more than 30s, then call the api again, you should received an 500 error because the jwt token had been expired.

 #zoom

See Also#

References#