JWT Authentication using Spring Security OAuth2 in Spring Boot Example

In this tutorial, we will learn how to use Spring Security OAuth2 for role-based JWT authentication in Spring Boot. In this example, we will create a sample application in which a user can sign up, login to retrieve JWT token, and use that JWT token to access secured REST APIs.

JWT is short for JSON Web Token. It is an open standard for securely transmitting information between parties as a JSON object. Because it is digitally signed, this information can be checked and trusted. JWTs can be signed with either a secret (HMAC) or a public/private key pair using RSA or ECDSA.

Follow the steps below to complete this tutorial:

Create a Spring Boot Application

  1. Go to Spring Initializr at https://start.spring.io and create a Spring Boot application with details as follows:
    • Project: Choose Gradle Project or Maven Project.
    • Language: Java
    • Spring Boot: Latest stable version of Spring Boot is selected by default. So leave it as is.
    • Project Metadata: Provide group name in the Group field. The group name is the id of the project. In Artifact field, provide the name of your project. In the package field, provide package name for your project. Next, select your preferred version of Java that is installed on your computer and is available on your local environment.
    • Dependencies: Add dependencies for Spring Web, Spring Security, OAuth2 Resource Server, Lombok, MySQL Driver, Spring Data JPA, and Spring Boot DevTools.

    Refer to the image below for example:

  2. Click the GENERATE button and save/download the project zip bundle.
  3. Extract the project to your preferred working directory.
  4. Import the project to your preferred Java development IDE such as Eclipse or IntelliJ IDEA.

Install Dependencies for JWT Support

For JWT support, we need jsonwebtoken library for Java Spring Boot. Find Maven Repository for jsonwebtoken at https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt. The jsonwebtoken library is used to generate, decode, verify JWT tokens.

For Gradle

Add the following dependency to your build.gradle file:


implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'

For Maven

Add the following dependency to your pom.xml file:



Generate a Java Keystore

To sign JWT, RSA private key is required and to validate a JWT, RSA public key corresponding to the RSA private key is required.

Java comes with keytool that helps to generate a key pair.

In windows, open a command prompt and go to C:\Program Files\Java\jdk-11.0.13\bin\ and execute the following command to generate a keystore. Here, E:/keystore.jks is the destination location and filename:


.\keytool -genkey -alias signjwt -keyalg RSA -keystore E:/keystore.jks - keysize 2048 -validity 365000    

Add Configurations to properties file

After the keystore.jks file is generated, create a new folder called keys inside the resources directory of your project and copy the keystore.jks file to /resources/keys/ folder.

Add the following configurations to your application.properties file:


#port on which the application should run
server.port= 8000

#mysql database connection
spring.datasource.url = jdbc:mysql://localhost:3306/test_db
spring.datasource.username = root
spring.datasource.password = Testing123$
spring.datasource.timeBetweenEvictionRunsMillis = 60000
spring.datasource.maxIdle = 1
#below properties will automatically creates and updates database schema
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update

#public private keys
app.jwt.keystore-location=keys/test-keystore.jks
app.jwt.keystore-password=testing123
app.jwt.key-alias=signingjwt
app.jwt.private-key-passphrase=testing123

Creating Entity classes

Create a Java class called User to map with the corresponding user table in the database for storing user information and add the following code:


package com.jwt.authentication.sample.user.domain;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "user")
public class User implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Column(name = "username", nullable = false, unique = true, length = 500)
	private String username;

	@Column(name = "email", nullable = false, unique = true, length = 500)
	private String email;

	@Column(name = "password", nullable = false, length = 500)
	private String password;

	@Column(name = "first_name", nullable = true, length = 500)
	private String firstName;

	@Column(name = "last_name", nullable = true, length = 500)
	private String lastName;

	@ Default
	@OneToMany(mappedBy = "user", targetEntity = Role.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
	private Set<Role> roles = new HashSet<>();

	@CreatedDate
	@Column(name = "created", nullable = true)
	private LocalDateTime created;

	@LastModifiedDate
	@Column(name = "modified", nullable = true)
	private LocalDateTime modified;
}

Create another Java class called Role to map with the corresponding role table in the database and add the following code:


package com.jwt.authentication.sample.user.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "role")
public class Role {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Column(name = "name", nullable = true, unique = false, length = 100)
	private String name;

	@ManyToOne(fetch = FetchType.LAZY, optional = false)
	@JoinColumn(name = "user_id", nullable = false, updatable = false)
	private User user;

}

Create a Java class called Post to map with the corresponding post table in the database in which we will perform CRUD operations via secured REST APIs using a JWT user token:


package com.jwt.authentication.sample.post.domain;

import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "post")
public class Post {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Column(name = "title", nullable = false, length = 500)
	private String title;

	@Column(name = "body", nullable = true, length = 5000)
	private String body;

	@Column(name = "path", nullable = true, length = 500)
	private String path;

	@ Default
	@Column(name = "deleted")
	private boolean deleted = false;
	
	@CreatedDate
	@Column(name = "created", nullable = true)
	private LocalDateTime created;

	@LastModifiedDate
	@Column(name = "modified", nullable = true)
	private LocalDateTime modified;

	@CreatedBy
	@Column(name = "createdBy", nullable = true)
	private Long createdBy;

	@LastModifiedBy
	@Column(name = "modifiedBy", nullable = true)
	private Long modifiedBy;

}

Creating DTO (Data Transfer Object) Classes

Create a DTO class called LoginDto. This class will be a getter setter class as follows:


package com.jwt.authentication.sample.user.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class LoginDto {
	private String username;
	private String password;
	private String token;
}

Create a DTO class called SignUpDto. This class will be a getter setter class as follows:


package com.jwt.authentication.sample.user.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class SignUpDto {
	private String email;
	private String username;
	private String password;
	private String status;
}

Create a DTO class called PostDto. This class will be a getter setter class as follows:


package com.jwt.authentication.sample.post.dto;

import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
public class PostDto {
	private Long postId;
	private String title;
	private String body;
	private String path;
	private Boolean deleted;
	private Long createdBy;
	private Long modifiedBy;
	private LocalDateTime created;
	private LocalDateTime modified;
}

Creating Repository Interfaces

Next, create a new interface called UserRepository that will act as a JPA repository for the User entity with the following code:


package com.jwt.authentication.sample.user.repository;

import org.springframework.stereotype.Repository;
import org.springframework.data.repository.CrudRepository;
import com.jwt.authentication.sample.user.domain.User;

@Repository
public interface UserRepository extends CrudRepository<User, Integer> {
    User findByEmail(String email);
    User findByUsername(String username);
}

Create another new interface called RoleRepository that will act as a JPA repository for the Role entity with the following code:


package com.jwt.authentication.sample.user.repository;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.jwt.authentication.sample.user.domain.Role;

@Repository
public interface RoleRepository extends CrudRepository<Role, Integer> {
	
}

Create a new interface called PostRepository that will act as a JPA repository for the Post entity with the following code:


package com.jwt.authentication.sample.post.repository;

import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.jwt.authentication.sample.post.domain.Post;

@Repository
public interface PostRepository extends CrudRepository<Post, Long> {
	Page<Post> findByCreatedByAndDeletedIsFalse(Long userId, Pageable pageable);
    Optional<Post> findByIdAndDeletedIsFalse(Long postId);
}

Creating Security Configuration Classes

Create a configuration class to retrieve private and public keys from the keystore.jks file as shown below:


package com.jwt.authentication.sample.config;

import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@Configuration
public class KeystoreConfig {
	private static final Logger log = LoggerFactory.getLogger(KeystoreConfig.class);

	@Value("${app.jwt.keystore-location}")
	private String keyStorePath;

	@Value("${app.jwt.keystore-password}")
	private String keyStorePassword;

	@Value("${app.jwt.key-alias}")
	private String keyAlias;

	@Value("${app.jwt.private-key-passphrase}")
	private String privateKeyPassphrase;

	@Bean
	public KeyStore getKeyStore() {
		try {
			KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
			InputStream resourceAsStream = Thread.currentThread().getContextClassLoader()
					.getResourceAsStream(keyStorePath);
			keyStore.load(resourceAsStream, keyStorePassword.toCharArray());
			return keyStore;
		} catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) {
			log.error("Unable to load keystore: {}", keyStorePath, e);
		}

		throw new IllegalArgumentException("Unable to load keystore");
	}

	@Bean
	public RSAPrivateKey getJwtSigningPrivateKey() {
		try {
			Key key = getKeyStore().getKey(keyAlias, privateKeyPassphrase.toCharArray());
			if (key instanceof RSAPrivateKey) {
				return (RSAPrivateKey) key;
			}
		} catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
			log.error("Unable to load private key from keystore: {}", keyStorePath, e);
		}

		throw new IllegalArgumentException("Unable to load private key");
	}

	@Bean
	public RSAPublicKey getJwtValidationPublicKey(KeyStore keyStore) {
		try {
			Certificate certificate = keyStore.getCertificate(keyAlias);
			PublicKey publicKey = certificate.getPublicKey();

			if (publicKey instanceof RSAPublicKey) {
				return (RSAPublicKey) publicKey;
			}
		} catch (KeyStoreException e) {
			log.error("Unable to load private key from keystore: {}", keyStorePath, e);
		}

		throw new IllegalArgumentException("Unable to load RSA public key");
	}

	@Bean
	public JwtDecoder jwtDecoder(RSAPublicKey rsaPublicKey) {
		return NimbusJwtDecoder.withPublicKey(rsaPublicKey).build();
	}
}

Create a class to implement jsonwebtoken library functions. This class contains methods to generate jwt tokens, get username from token, and other useful methods:


package com.jwt.authentication.sample.config;

import java.util.Date;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@ Component
public class JwtManager {
	// 25 hours expiry time
	public static final long JWT_TOKEN_VALIDITY_IN_HOUR = 24 * 60 * 60;

	@Autowired
	private KeystoreConfig keystoreConfig;

	public String generateToken(Map<String, Object> claims, String subject) {

		return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
				.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY_IN_HOUR * 1000))
				.signWith(SignatureAlgorithm.RS256, keystoreConfig.getJwtSigningPrivateKey()).compact();
	}

	// get username from token
	public String getUsernameFromToken(String token) {
		return getClaimFromToken(token, Claims::getSubject);
	}

	// get user id from token
	public String getIdFromToken(String token) {
		return getClaimFromToken(token, Claims::getId);
	}

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

	// get information from token
	private Claims getAllClaimsFromToken(String token) {
		return Jwts.parser().setSigningKey(keystoreConfig.getJwtSigningPrivateKey()).parseClaimsJws(token).getBody();
	}

	// validate token
	public Boolean validateToken(String token, UserDetails userDetails) {
		final String username = getUsernameFromToken(token);
		return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
	}

	// get expiration time from jwt token
	public Date getExpirationDateFromToken(String token) {
		return getClaimFromToken(token, Claims::getExpiration);
	}

	// check if token is expired
	private Boolean isTokenExpired(String token) {
		final Date expiration = getExpirationDateFromToken(token);
		return expiration.before(new Date());
	}

}

Create a class to implement org.springframework.security.core.userdetails.UserDetails interface:


package com.jwt.authentication.sample.auth;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.jwt.authentication.sample.user.domain.User;

public class CustomUserDetails implements UserDetails {
	private static final long serialVersionUID = 1L;

	private User user;

	public CustomUserDetails(User user) {
		this.user = user;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

		List<String> authorityList = user.getRoles().stream().map(a -> a.getName()).collect(Collectors.toList());
		for (String authority : authorityList)
			authorities.add(new SimpleGrantedAuthority(authority));
		return authorities;
	}

	@Override
	public String getPassword() {

		return user.getPassword();
	}

	@Override
	public String getUsername() {
		return user.getUsername();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	public long getId() {
		return user.getId();
	}

	public String getEmail() {
		return user.getEmail();
	}
}

Create a class to implement org.springframework.security.core.userdetails.UserDetailsService interface:


package com.jwt.authentication.sample.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.jwt.authentication.sample.user.domain.User;
import com.jwt.authentication.sample.user.repository.UserRepository;
@Service
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Override
	public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsername(username);
		if (user == null) {
			throw new UsernameNotFoundException("User not found");
		}
		return new CustomUserDetails(user);
	}

}

Create a WebSecurityConfig class and extend it with WebSecurityConfigurerAdapter abstract class to implement web and http security:


package com.jwt.authentication.sample.config;

import javax.servlet.http.HttpServletResponse;
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.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.web.authentication.UsernamePasswordAuthenticationFilter;
import com.jwt.authentication.sample.auth.CustomUserDetailsService;

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private JwtSecurityFilter jwtSecurityFilter;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.cors().and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
				.and()
				.authorizeRequests(configurer -> configurer.antMatchers("/users/signup", "/users/login").permitAll()
						.anyRequest().authenticated())
				.addFilterBefore(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class).exceptionHandling()
				.authenticationEntryPoint((request, response, ex) -> {
					response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
				});
	}

	@Bean
	public UserDetailsService userDetailsService() {
		return new CustomUserDetailsService();
	}

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

}

Create a new class and extend it with OncePerRequestFilter abstract class. This class will be responsible to check all incoming requests and allow access to secured REST APIs to only those requests with a valid JWT token. The code is as follows:


package com.jwt.authentication.sample.config;

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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.jwt.authentication.sample.auth.CustomUserDetails;
import com.jwt.authentication.sample.auth.CustomUserDetailsService;
import io.jsonwebtoken.ExpiredJwtException;

@ Component
public class JwtSecurityFilter extends OncePerRequestFilter {

	@Autowired
	private JwtManager jwtManager;
	@Autowired
	private CustomUserDetailsService customUserDetailsService;

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

		String username = null;
		String jwtToken = null;
		// get only token if the token is in the form of "Bearer token"
		if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
			jwtToken = requestTokenHeader.substring(7);
			try {
				username = jwtManager.getUsernameFromToken(jwtToken);
			} catch (IllegalArgumentException e) {
				logger.info("Unable to get JWT Token");
			} catch (ExpiredJwtException e) {
				logger.info("JWT Token has expired");
			}
		} else {
			logger.warn("JWT Token does not begin with Bearer String");
		}

		// validate the token
		if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

			CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

			// if token is valid configure Spring Security to manually set authentication
			if (jwtManager.validateToken(jwtToken, userDetails)) {

				UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
						userDetails, null, userDetails.getAuthorities());
				usernamePasswordAuthenticationToken
						.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

				SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
			}
		}
		filterChain.doFilter(request, response);
	}
}

Creating Service Interfaces

Create a UserService interface. This class will be contain methods for user sign up and login:


package com.jwt.authentication.sample.user.service;

import com.jwt.authentication.sample.user.dto.LoginDto;
import com.jwt.authentication.sample.user.dto.SignUpDto;

public interface UserService {
    SignUpDto signUp(SignUpDto signUpDto);
    LoginDto login(LoginDto loginRequest);
}

Create a PostService interface. This class will contain methods to perform CRUD operations for secured REST APIs:


package com.jwt.authentication.sample.post.service;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.jwt.authentication.sample.post.dto.PostDto;

public interface PostService {
    PostDto create(PostDto postDto);
    Page<PostDto> getPostsByUserId(Long userId, Pageable pageable);
    PostDto getPostById(Long postId);
    PostDto update(PostDto postDto);
    PostDto delete(Long postId);
}

Implementation of Service Interfaces

Create UserServiceImpl class to implement sign up and login methods of the UserService interface:


package com.jwt.authentication.sample.user.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import com.jwt.authentication.sample.auth.CustomUserDetails;
import com.jwt.authentication.sample.auth.CustomUserDetailsService;
import com.jwt.authentication.sample.config.JwtManager;
import com.jwt.authentication.sample.user.domain.Role;
import com.jwt.authentication.sample.user.domain.User;
import com.jwt.authentication.sample.user.dto.LoginDto;
import com.jwt.authentication.sample.user.dto.SignUpDto;
import com.jwt.authentication.sample.user.repository.RoleRepository;
import com.jwt.authentication.sample.user.repository.UserRepository;
import com.jwt.authentication.sample.user.service.UserService;

@Service
public class UserServiceImpl implements UserService {
	private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

	@Autowired
	private UserRepository userRepository;
	@Autowired
	private RoleRepository roleRepository;
	@Autowired
	private CustomUserDetailsService customUserDetailsService;
	@Autowired
	private PasswordEncoder passwordEncoder;
	@Autowired
	private JwtManager jwtManager;

	@Override
	public SignUpDto signUp(SignUpDto signUpDto) {
		log.info("calling signUp....");
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		String encodedPassword = passwordEncoder.encode(signUpDto.getPassword());

		User user = User.builder().email(signUpDto.getEmail()).username(signUpDto.getUsername())
				.password(encodedPassword).build();
		user = userRepository.save(user);

		List<Role> authorities = new ArrayList<>();
		authorities.add(Role.builder().user(user).name("USER").build());
		authorities.add(Role.builder().user(user).name("ADMIN").build());
		roleRepository.saveAll(authorities);

		return SignUpDto.builder().status("success").build();
	}

	@Override
	public LoginDto login(LoginDto loginDto) {
		log.info("calling login....");
		CustomUserDetails userDetails;
		try {
			userDetails = customUserDetailsService.loadUserByUsername(loginDto.getUsername());

		} catch (UsernameNotFoundException e) {
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found");
		}

		if (passwordEncoder.matches(loginDto.getPassword(), userDetails.getPassword())) {
			log.info("password matched");

			Map<String, Object> claims = new HashMap<>();
			claims.put("username", userDetails.getUsername());

			String authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority)
					.collect(Collectors.joining(","));
			claims.put("roles", authorities);
			claims.put("userId", userDetails.getId());
			String subject = userDetails.getUsername();
			String jwt = jwtManager.generateToken(claims, subject);
			return LoginDto.builder().token(jwt).build();
		}

		throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authorized");
	}

}

Here, we are using BCryptPasswordEncoder to encode the user's password so that only the hash value of the password is stored in the database and not the original password for better security.

Create PostServiceImpl class to implement methods of the PostSerivce interface:


package com.jwt.authentication.sample.post.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import com.jwt.authentication.sample.post.domain.Post;
import com.jwt.authentication.sample.post.dto.PostDto;
import com.jwt.authentication.sample.post.repository.PostRepository;
import com.jwt.authentication.sample.post.service.PostService;

@Service
public class PostServiceImpl implements PostService {
	@Autowired
	private PostRepository postRepository;

	@Override
	public PostDto create(PostDto postDto) {
		Post post = Post.builder().title(postDto.getTitle()).body(postDto.getBody()).path(postDto.getPath())
				.createdBy(postDto.getCreatedBy()).build();
		post = postRepository.save(post);
		return PostDto.builder().postId(post.getId()).title(post.getTitle()).body(post.getBody()).path(post.getPath())
				.createdBy(post.getCreatedBy()).build();
	}

	@Override
	public Page<PostDto> getPostsByUserId(Long userId, Pageable pageable) {
		Page<Post> posts = postRepository.findByCreatedByAndDeletedIsFalse(userId, pageable);
		List<PostDto> postDtos = new ArrayList<>();
		posts.forEach(p -> {
			postDtos.add(PostDto.builder().title(p.getTitle()).postId(p.getId()).body(p.getBody()).path(p.getPath())
					.deleted(p.isDeleted()).createdBy(p.getCreatedBy()).modifiedBy(p.getModifiedBy())
					.created(p.getCreated()).modified(p.getModified()).build());
		});
		return new PageImpl<PostDto>(postDtos, pageable, posts.getTotalElements());
	}

	@Override
	public PostDto getPostById(Long postId) {
		Optional<Post> optPost = postRepository.findByIdAndDeletedIsFalse(postId);
		if (optPost.isPresent()) {
			Post post = optPost.get();
			return PostDto.builder().title(post.getTitle()).postId(post.getId()).body(post.getBody())
					.path(post.getPath()).deleted(post.isDeleted()).createdBy(post.getCreatedBy())
					.modifiedBy(post.getModifiedBy()).created(post.getCreated()).modified(post.getModified()).build();
		}
		return null;
	}

	@Override
	public PostDto update(PostDto postDto) {
		Optional<Post> optPost = postRepository.findByIdAndDeletedIsFalse(postDto.getPostId());
		if (optPost.isEmpty()) {
			throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found");
		}
		Post post = optPost.get();
		post = post.toBuilder().title(postDto.getTitle()).body(postDto.getBody()).path(postDto.getPath()).build();
		post = postRepository.save(post);
		return PostDto.builder().title(post.getTitle()).postId(post.getId()).body(post.getBody()).path(post.getPath())
				.deleted(post.isDeleted()).createdBy(post.getCreatedBy()).modifiedBy(post.getModifiedBy())
				.created(post.getCreated()).modified(post.getModified()).build();
	}

	@Override
	public PostDto delete(Long postId) {
		Optional<Post> optPost = postRepository.findByIdAndDeletedIsFalse(postId);
		if (optPost.isEmpty()) {
			throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found");
		}
		Post post = optPost.get();
		post.setDeleted(true);
		post = postRepository.save(post);
		return PostDto.builder().title(post.getTitle()).postId(post.getId()).body(post.getBody()).path(post.getPath())
				.deleted(post.isDeleted()).createdBy(post.getCreatedBy()).modifiedBy(post.getModifiedBy())
				.created(post.getCreated()).modified(post.getModified()).build();
	}

}

Create Utility Classes

Create a utility class to get the details of a current logged-in user:


package com.jwt.authentication.sample.user.util;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.jwt.authentication.sample.auth.CustomUserDetails;

public class UserUtil {

	public static CustomUserDetails getCurrentLoggedInUser() {
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
		return userDetails;
	}
}

Create Controller Classes

Create a class with REST APIs to sign up and login users:


package com.jwt.authentication.sample.user.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.jwt.authentication.sample.user.dto.LoginDto;
import com.jwt.authentication.sample.user.dto.SignUpDto;
import com.jwt.authentication.sample.user.service.UserService;

@RestController
@RequestMapping(path = "/users")
public class UserController {
	@Autowired
	private UserService userService;

	@PostMapping(path = "/signup", consumes = { MediaType.APPLICATION_JSON_VALUE })
	public ResponseEntity<SignUpDto> signUp(@RequestBody  SignUpDto signUpRequest) {
		return ResponseEntity.ok(userService.signUp(signUpRequest));
	}

	@PostMapping(path = "/login", consumes = { MediaType.APPLICATION_JSON_VALUE })
	public ResponseEntity<LoginDto> login(@RequestBody  LoginDto loginDto) {
		return ResponseEntity.ok(userService.login(loginDto));
	}

}

Create a Java class for with secured REST endpoints to perform CRUD operations in database. Only a request with a valid JWT user token will be able to access these APIs:


package com.jwt.authentication.sample.post.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortDefault;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.jwt.authentication.sample.auth.CustomUserDetails;
import com.jwt.authentication.sample.post.dto.PostDto;
import com.jwt.authentication.sample.post.service.PostService;
import com.jwt.authentication.sample.user.util.UserUtil;

@RequestMapping(path = "/api/posts")
@RestController
public class PostController {
	@Autowired
	private PostService postService;

	@PreAuthorize("hasAuthority('USER')")
	@PostMapping(path = "/create", consumes = { MediaType.APPLICATION_JSON_VALUE })
	public ResponseEntity<PostDto> create(@RequestBody  PostDto postDto) {
		CustomUserDetails loggedInUser = UserUtil.getCurrentLoggedInUser();
		postDto.setCreatedBy(loggedInUser.getId());
		return ResponseEntity.ok(postService.create(postDto));
	}

	@PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
	@GetMapping
	public ResponseEntity<Page<PostDto>> getPosts(@PageableDefault(page = 0, size = 30) @SortDefault.SortDefaults({
			@SortDefault(sort = "created", direction = Sort.Direction.DESC) }) Pageable pageable) {
		CustomUserDetails loggedInUser = UserUtil.getCurrentLoggedInUser();
		return ResponseEntity.ok(postService.getPostsByUserId(loggedInUser.getId(), pageable));
	}

	@PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
	@GetMapping(path = "/{postId}")
	public ResponseEntity<PostDto> getPostById(@PathVariable(name = "postId") Long postId) {
		return ResponseEntity.ok(postService.getPostById(postId));
	}

	@PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
	@GetMapping(path = "/users/{userId}")
	public ResponseEntity<Page<PostDto>> getPostsByUserId(@PathVariable(name = "userId") Long userId,
			@PageableDefault(page = 0, size = 30) @SortDefault.SortDefaults({
					@SortDefault(sort = "created", direction = Sort.Direction.DESC) }) Pageable pageable) {
		return ResponseEntity.ok(postService.getPostsByUserId(userId, pageable));
	}

	@PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
	@PutMapping(path = "/update", consumes = { MediaType.APPLICATION_JSON_VALUE })
	public ResponseEntity<PostDto> update(@RequestBody  PostDto postDto) {
		return ResponseEntity.ok(postService.update(postDto));
	}

	@PreAuthorize("hasAuthority('ADMIN')")
	@DeleteMapping(path = "/delete/{postId}", consumes = { MediaType.APPLICATION_JSON_VALUE })
	public ResponseEntity<PostDto> delete(@PathVariable(name = "postId") Long postId) {
		return ResponseEntity.ok(postService.delete(postId));
	}
}

Annotate Main Class

Annotate the main Spring Boot class with @EnableJpaAuditing annotation. Example below:


package com.jwt.authentication.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class JwtAuthenticationSpringBootSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(JwtAuthenticationSpringBootSampleApplication.class, args);
	}

}

Sign Up User

Generate JWT Token

Access Secured REST Apis