Spring Boot Security with JWT Example

In this article let us learn about Json Web Tokens (JWT), How to generate JWT token and to refresh the JWT token.

We are going to use Spring Boot database authentication and JWT token generation, validation and token refresh.

We have discussed regarding Spring Boot Security with database authentication in our previous article.

For Spring Boot Security database authentication please refer here.

We are going to cover – Spring Boot Security with JWT Example – Token Generation, Token Validation and Token Refresh.

What is JWT?

JWT stands for Json Web Token which is a token implementation in JSON format.

Json tokens used for authentication and data sharing between parties.

The JWT has 3 parts,

Header, Payload and Signature.

Example of JSON webtoken,

Sample JSON webtoken

What does these 3 parts contain?

Header: Information regarding the token. (Such as algorithm used to construct it)

Payload: has the information related to user (issuer, expirationTime etc.)

Signature: Is used to see if the token has been changed. This is an optional part.

Advantages of JWT:

  • Is a stateless mechanism, which does not store any user related information in database. Essential information about the user from the json webtoken without having to communicate with the database.
  • JWT helps in the prevention of cross-site request forgery (CSRF) threats. (CSRF).
  • JWT is compact, it can be sent via URL/Post request/HttpHeader

Disadvantages of JWT:

  • JWT relies on single key, if accidently key is leaked the system will be compromised.
  • JWT token is a short lived one, It is frequently required to recreate the token on expiration.

Project Structure:

Project Structure

As we have already covered the Authentication in our previous article, we are going to discuss only with respect to JWT.

If you have missed it, Kindly read this.

What are we going to do here?

We are going to create 2 users and login with them.

After the user is successfully authenticated, we will generate a couple of JWT tokens.

The first token will have a shorter expiry period compared with the second token (Refresh Token – more expiry period).

First and Second token will be added to the response header.

From the next API call for which user have access, the access is provided through JWT token validation.

In most cases, tokens will expire after a set length of time. In this scenario, we’ll create an API called “/refreshToken” that will validate the refresh token and deliver a new JSON token after the user has been authenticated.

Next, construct two filters: one for token production and the other for validation. All the requests will be intercepted by filter and if the user is logging in a new token will be generated or token will be validated if the user has already logged in.

JwtTokenCreator.java


package com.javainfinite.security.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;

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

public class JwtTokenCreator {

    Logger logger = LoggerFactory.getLogger(JwtTokenCreator.class);

    public void generateToken(HttpServletRequest request, HttpServletResponse response) {

        //Get the username from authentication object
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication != null) { //verify whether user is authenticated
            String username = authentication.getName();
            SecretKey key = Keys.hmacShaKeyFor(SecurityContants.JWT_KEY.getBytes(StandardCharsets.UTF_8));

            String jwt_token = Jwts.builder()
                    .setIssuer("javainfinite")
                    .setExpiration(new Date((new Date()).getTime() + 300000))
                    .setSubject("javainfinite_token")
                    .claim("username", username)
                    .claim("authorities", getStudentRoles((List<GrantedAuthority>) authentication.getAuthorities()))
                    .signWith(key)
                    .compact();

            if (request.getHeader(SecurityContants.REFRESH_HEADER) == null) {

                String refresh_token = Jwts.builder()
                        .setIssuer("javainfinite")
                        .setExpiration(new Date((new Date()).getTime() + 3000000))
                        .setSubject("javainfinite_token")
                        .claim("username", username)
                        .claim("authorities", getStudentRoles((List<GrantedAuthority>) authentication.getAuthorities()))
                        .signWith(key)
                        .compact();

                response.setHeader(SecurityContants.REFRESH_HEADER, refresh_token);
                logger.info("Refresh Token successfully generated: {}", refresh_token);
            }
            response.setHeader(SecurityContants.AUTHORIZATION_HEADER, jwt_token);
            logger.info("Token successfully generated: {}", jwt_token);
        }
    }

    private String getStudentRoles(List<GrantedAuthority> collection) {
        Set<String> authoritiesSet = new HashSet<>();
        for (GrantedAuthority authority : collection) {
            authoritiesSet.add(authority.getAuthority());
        }
        return String.join(",", authoritiesSet);
    }
}

We will authenticate the user, Spring Security will automatically store the user’s details in Security Context.

We are getting the user details from Security Context as Authenticated Object and if that object is null – the user has not logged in.

If user is authenticated, we will create a secret key based on our own custom key with the help of Keys class.

We are generating a secret key here,


SecretKey key = Keys.hmacShaKeyFor(SecurityContants.JWT_KEY.getBytes(StandardCharsets.UTF_8));

We can generate a new token by configuring with necessary information like issuer, subject and expiration time, etc. It is a good practice to have expiration time to 15 minutes for the token.

Next we are setting claims, user information like username and his roles in authorities.

After token is generated we will set it with response header.

jwt_token has less expiration time where as refresh token has more expiration time.

SecurityConstants.java


package com.javainfinite.security.util;

public class SecurityContants {

    public static final String JWT_KEY = "jxgEQe.XHuPq8VdbyYFNkAN.dudQ0903YUn4";
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String REFRESH_HEADER = "RefreshToken";
}

Next we are going to create a Json token validator class,

JwtTokenValidator.java


package com.javainfinite.security.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class JwtTokenValidator {

    Logger logger = LoggerFactory.getLogger(JwtTokenValidator.class);

    public void validateJwtToken(HttpServletRequest request, HttpServletResponse response, boolean isRefreshValidation) {

        String token = request.getHeader(SecurityContants.AUTHORIZATION_HEADER);
        String refresh = request.getHeader(SecurityContants.REFRESH_HEADER);

        logger.info("Authorization Token: {}", token);

        if (token != null && !token.contains("Basic")) {
            try {
                SecretKey key = Keys.hmacShaKeyFor(
                        SecurityContants.JWT_KEY.getBytes(StandardCharsets.UTF_8));

                Claims claims = Jwts.parserBuilder()
                        .setSigningKey(key)
                        .build()
                        .parseClaimsJws(isRefreshValidation ? refresh : token)
                        .getBody();

                String username = String.valueOf(claims.get("username"));
                String authorities = (String) claims.get("authorities");

                Authentication auth = new UsernamePasswordAuthenticationToken(username, null,
                        getStudentRoles(authorities));

                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (ExpiredJwtException ex) {
                logger.info("Token expired!");

            } catch (Exception e) {
                throw new BadCredentialsException("Invalid Token received!");
            }
        }

    }

    private List<GrantedAuthority> getStudentRoles(String authorities) {
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        String[] roles = authorities.split(",");
        for (String role : roles) {
            grantedAuthorityList.add(new SimpleGrantedAuthority(role.replaceAll("\\s+", "")));
        }

        return grantedAuthorityList;
    }
}

From the code,

We are getting the authorization from the header and forming a Secretkey same way as we did in Token Generator.

                
Claims claims = Jwts.parserBuilder()
                        .setSigningKey(key)
                        .build()
                        .parseClaimsJwt(isRefreshToken ? refresh : authorization)
                        .getBody();

Our token will be validated by this piece of code – To verify if token expired or bad credentials.

After successful validation, we get the user information like username and authorities, reform the authentication object and will set them in SecurityContext.

Creating filters,

JwtTokenCreatorFilter.java


package com.javainfinite.security.filter;

import com.javainfinite.security.util.JwtTokenCreator;
import com.javainfinite.security.util.SecurityContants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtTokenCreatorFilter extends OncePerRequestFilter {

    Logger logger = LoggerFactory.getLogger(JwtTokenCreatorFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.info("Token generator class");
        JwtTokenCreator creator = new JwtTokenCreator();
        creator.generateToken(request, response);
        filterChain.doFilter(request, response);
    }
}

We have created a filter for token creator and called our util method for token creation.

JwtTokenValidationFilter.java


package com.javainfinite.security.filter;

import com.javainfinite.security.util.JwtTokenValidator;
import com.javainfinite.security.util.SecurityContants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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;

public class JwtTokenValidationFilter extends OncePerRequestFilter {

    Logger logger = LoggerFactory.getLogger(JwtTokenValidationFilter.class);

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

        logger.info("Request received for Jwt token validation");
        JwtTokenValidator validator = new JwtTokenValidator();
        validator.validateJwtToken(request, response, false);
        filterChain.doFilter(request, response);

    }
}

Next we have to add these filters to our StudentSecurityConfig,

StudentSecurityConfig.java


package com.javainfinite.security.security;

import com.javainfinite.security.filter.JwtTokenCreatorFilter;
import com.javainfinite.security.filter.JwtTokenValidationFilter;
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;

@Configuration
public class StudentSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(new JwtTokenValidationFilter(), BasicAuthenticationFilter.class)
                .addFilterAfter(new JwtTokenCreatorFilter(), BasicAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/studentInfo").authenticated()
                .antMatchers("/register").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/getStudentRoles").hasAuthority("ROLE_WRITE")
                .and()
                .httpBasic().and().csrf().disable();
    }

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

StudentController.java


package com.javainfinite.security.controller;

import com.javainfinite.security.model.Student;
import com.javainfinite.security.service.StudentService;
import com.javainfinite.security.util.JwtTokenCreator;
import com.javainfinite.security.util.JwtTokenValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

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

@RestController
public class StudentController {

    Logger logger = LoggerFactory.getLogger(StudentController.class);

    private StudentService service;

    private PasswordEncoder encoder;

    public StudentController(StudentService service, PasswordEncoder encoder) {
        this.service = service;
        this.encoder = encoder;
    }

    /**
     * Any user can access this API - No Authentication required
     *
     * @param student
     * @return
     */

    @PostMapping("/register")
    public Student registerStudent(@RequestBody Student student) {
        Student student1 = new Student();
        logger.info("Student name: {}", student.getSname());
        student1.setSname(student.getSname());
        student1.setPassword(encoder.encode(student.getPassword()));
        student1.setSrole(student.getSrole());
        return service.register(student1);
    }

    /**
     * user login
     * @return
     */
    @PostMapping("/login")
    public String login(){
        return "Successfully logged in by user: "+SecurityContextHolder.getContext().getAuthentication().getName();
    }

    /**
     * User who has logged in successfully can access this API
     *
     * @param username
     * @return
     */
    @GetMapping("/studentInfo")
    public Student getStudentInfo(@RequestParam("sname") String username) {
        return service.getDetails(username);
    }

    /**
     * User who has the role ROLE_WRITE can only access this API
     *
     * @param username
     * @return
     */
    @GetMapping("/getStudentRoles")
    public String getStudentRoles(@RequestParam("sname") String username) {
        return service.getStudentRoles(username);
    }

    /**
     * Token expired, validate the refresh token and then generate a new token
     * @param request
     * @param response
     */
    @GetMapping("/refreshToken")
    public void refreshToken(HttpServletRequest request, HttpServletResponse response) {
        logger.info("Inside refresh token...");
        JwtTokenCreator generator = new JwtTokenCreator();
        JwtTokenValidator validation = new JwtTokenValidator();
        validation.validateJwtToken(request, response, true);
        generator.generateToken(request, response);
    }

}

No changes made in Student Authentication Provider,

StudentAuthenticationProvider.java


package com.javainfinite.security.security;

import com.javainfinite.security.model.Student;
import com.javainfinite.security.repository.StudentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class StudentAuthenticationProvider implements AuthenticationProvider {

    Logger logger = LoggerFactory.getLogger(StudentAuthenticationProvider.class);

    private StudentRepository repository;

    private PasswordEncoder encoder;

    public StudentAuthenticationProvider(StudentRepository repository, PasswordEncoder encoder) {
        this.encoder = encoder;
        this.repository = repository;
    }

    /**
     * Get the username and password from authentication object and validate with password encoders matching method
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        Student student = repository.findBySname(username);
        if (student == null) {
            throw new BadCredentialsException("Details not found");
        }

        if (encoder.matches(password, student.getPassword())) {
            logger.info("Successfully Authenticated the user");
            return new UsernamePasswordAuthenticationToken(username, password, getStudentRoles(student.getSrole()));
        } else {
            throw new BadCredentialsException("Password mismatch");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

    public List<GrantedAuthority> getStudentRoles(String studentRoles) {
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        String[] roles = studentRoles.split(",");
        for (String role : roles) {
            grantedAuthorityList.add(new SimpleGrantedAuthority(role.replaceAll("\\s+", "")));
        }
        return grantedAuthorityList;
    }
}

StudentService.java


package com.javainfinite.security.service;

import com.javainfinite.security.model.Student;
import com.javainfinite.security.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class StudentService {

    @Autowired
    private StudentRepository studentRepository;

    public Student register(Student student) {
        return studentRepository.save(student);
    }

    public Student getDetails(String username) {
        return studentRepository.findBySname(username);
    }

    public String getStudentRoles(String username) {
        return studentRepository.findBySname(username).getSrole();
    }
}

StudentRepository.java


package com.javainfinite.security.repository;

import com.javainfinite.security.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    public Student findBySname(String username);
}

Student.java


package com.javainfinite.security.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Student {

    @Id
    @GeneratedValue
    private Integer id;

    @Column(name = "username")
    private String sname;

    @Column(name = "password")
    private String password;

    @Column(name = "roles")
    private String srole;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSrole() {
        return srole;
    }

    public void setSrole(String srole) {
        this.srole = srole;
    }
}

Now let us run the application,

First let us register 2 users, Alpha who has the authority – ROLE_READ and ROLE_WRITE and user beta who has only ROLE_READ.

Both users will be able to access “/studentInfo”.

Only Alpha user will be able to access “/getStudentRoles”.

Registering the users,

user 1 – alpha
user 2 – beta

Login with user beta,

beta login

Trying to access “/getMyInfo”, without authentication we will get unauthorized error,

unauthorized error

Now let us try to add the JSON web token to the header as authorization,

From logs,

json token
No Auth – Token added to header

Now let us try to access “/getStudentRoles” where the user beta is restricted from accessing the API.

403 – forbidden

Beta user does not have access to the above mentioned API, we are getting 403 error.

Let us try to login as alpha and attach the web token to the header and try it again,

Alpha Login

As an alpha user, We are able to access the API.

If the token is expired, we will get 401 error in postman and in logs we will get the error message token expired.

Token Expired
Token expired

To get a new token, we can invoke “/refreshToken” API.

Security context will provide us with all the necessary information required for generating a new token for the authenticated user.

How can we tell if the authorised user is the same?

Refresh Token helps us with this. We will validate the refresh token and validate the user authenticity.

We will validate the refresh token and if it is valid we will generate a new token or we will throw an exception.

Refresh Token

Now with this new generated token we will be able to access the API’s.

We have successfully authenticated and authorized our application with the help of JWT token. (Spring Boot Security with JWT Example)

Code available at here.

By Sri

Leave a Reply

Your email address will not be published. Required fields are marked *