Get Started with JWT Authentication in Spring Boot

Pushpanshu Ranjan Singh
5 min readSep 7, 2023

--

JWT is a secure way of exchanging information between entities as a JSON Object. The information can be verified and trusted as it is digitally signed. JWT can be signed by using HMAC Algorithm or public/private key pair using RSA.

Content

  1. Version
  2. Dependencies
  3. SecurityFilterChain Configuration
  4. JWT Configuration [Provider and Validator]
  5. Implementation within Services
  6. UserServiceImplementation [Custom]

Version

Java : v17
Spring Boot: v3.1.3
Spring Security: v6.1.2

Dependencies

<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>

These dependencies need to added along with spring security and spring web.

SecurityFilterChain Configuration

Explanation of few terms

Spring Security provides default form login basic authentication where you can login on the web with default username: ‘user’ and password: ‘GENERATED PASSWORD IN CONSOLE’ but we need custom authentication process so we will override the default configuration.

Line 8 and 9: It means, what so ever path you provide to requestmatcher it will validate and verify request for that, other than that verification is not required.

Line 10: SessionCreationPolicy is set to be STATELESS, it means no session will be saved on Server Side and It doesn’t require to send session id along with request.
JWT solves such hectic job in-built like Session, Expiration etc.

Line 11: Check for the Basic Authentication and later on validated the JWT before requesting any request.

Line 17: Set the scope from where a client can request for information.

JWT Configuration [Provider and Validator]

Validator: Server side JWT Token validation happen, where it takes the token from header, parse the token token data with the help of secret key to get the information.

public class JWTValidator extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain)
throws ServletException, IOException {
String jwt = request.getHeader(JWTConstant.JWT_HEADER);
if (jwt != null) {
jwt = jwt.substring(7);
try {
SecretKey key = Keys.hmacShaKeyFor(JWTConstant.SECRET_KEY.getBytes());
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody();
String email = String.valueOf(claims.get("email"));
String authorities = String.valueOf(claims.get("authorities"));
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
Authentication authentication = new UsernamePasswordAuthenticationToken(email, null, auths);
SecurityContextHolder.getContext().setAuthentication(authentication);

} catch (Exception e) {
throw new BadCredentialsException("Invalid Token from JWT Validator");
}

}
filterChain.doFilter(request, response);
}
}

Provider: It is for generating token and performing any operation based on token.

@Service
public class JWTProvider {
SecretKey key = Keys.hmacShaKeyFor(JWTConstant.SECRET_KEY.getBytes());
public String generateToken(Authentication auth){
return Jwts.builder()
.setIssuedAt(new Date())
.setExpiration(new Date(new Date().getTime()+846000000))
.claim("email",auth.getName())
.signWith(key).compact();
}

public String getEmailFromToken(String jwt){
jwt = jwt.substring(7);
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt).getBody();
return String.valueOf(claims.get("email"));
}

public JWTProvider() {
}
}

We should use Static Strings as constant so it don’t get messed up and easy to refactor so, here we keep the constant in different file.

public class JWTConstant {
public static final String SECRET_KEY = <any_long_key>;
public static final String JWT_HEADER = "Authorization";
}

CustomUserServiceImplementation

We need it because we override the default authentication configuration [SecurityFilterChain Configuration].

We are using email and password for authentication, so In that case we need to find the user by his/her email. Here it is overridden to achieve the User as we need. 👍

@Service
public class CustomUserServiceImplementation implements UserDetailsService {
private final UserRepository userRepository;

public CustomUserServiceImplementation(UserRepository userRepository) {
this.userRepository = userRepository;
}


@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Users users = userRepository.findByEmail(email);
if (users == null) {
throw new UsernameNotFoundException("user not found with this " + email);
}
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
return new org.springframework.security.core.userdetails.User(users.getEmail(), users.getPassword(), authorities);
}
}

Configuration Completed and Now to implement into authentication and other services for verification.

Authentication Controller

Token Generation on Login and Signup

@RestController
@RequestMapping("/auth")
public class AuthController {

private final UserRepository userRepository;
private final JWTProvider jwtProvider;
private final CustomUserServiceImplementation customUserServiceImplementation;
private final PasswordEncoder passwordEncoder;

public AuthController(UserRepository userRepository, JWTProvider jwtProvider, CustomUserServiceImplementation customUserServiceImplementation, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.jwtProvider = jwtProvider;
this.customUserServiceImplementation = customUserServiceImplementation;
this.passwordEncoder = passwordEncoder;
}

@PostMapping("/signup")
public ResponseEntity<AuthResponse> createUserHandler(@RequestBody Users users) throws UserException {

String email = users.getEmail();
String password = users.getPassword();
String firstName = users.getFirstName();
String lastname = users.getLastName();

Users isEmailExist = userRepository.findByEmail(email);

if (isEmailExist != null) {
throw new UserException("Email already exists with other account");
}

Users createdUsers = new Users();
createdUsers.setEmail(email);
createdUsers.setPassword(passwordEncoder.encode(password));
createdUsers.setFirstName(firstName);
createdUsers.setLastName(lastname);

Users savedUsers = userRepository.save(createdUsers);

Authentication authentication = new UsernamePasswordAuthenticationToken(savedUsers.getEmail(), savedUsers.getPassword());
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtProvider.generateToken(authentication);

AuthResponse authResponse = new AuthResponse();
authResponse.setJwt(token);
authResponse.setMessage("Signup Success");
return new ResponseEntity<>(authResponse, HttpStatus.CREATED);

}


@PostMapping("/login")
public ResponseEntity<AuthResponse> loginUserHandler(@RequestBody LoginRequest loginRequest) {
String email = loginRequest.getEmail();
String password = loginRequest.getPassword();

Authentication authentication = authenticate(email, password);
SecurityContextHolder.getContext().setAuthentication(authentication);

String token = jwtProvider.generateToken(authentication);

AuthResponse authResponse = new AuthResponse();
authResponse.setJwt(token);
authResponse.setMessage("Login Success");
return new ResponseEntity<>(authResponse, HttpStatus.OK);
}

private Authentication authenticate(String email, String password) {
UserDetails userDetails = customUserServiceImplementation.loadUserByUsername(email);

if (userDetails == null) {
throw new BadCredentialsException("Invalid Email");
}
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("Invalid Password");
}
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}

Let’s see it, How can we find the user from JWT

Interface

public interface UserService {
Users findUserProfileByJwt(String jwt) throws UserException;
}

Service

@Service
public class UserServiceImplementation implements UserService {
private final UserRepository userRepository;
private final JWTProvider jwtProvider;

public UserServiceImplementation(UserRepository userRepository, JWTProvider jwtProvider) {
this.userRepository = userRepository;
this.jwtProvider = jwtProvider;
}

@Override
public Users findUserProfileByJwt(String jwt) throws UserException {
String email = jwtProvider.getEmailFromToken(jwt);
Users user = userRepository.findByEmail(email);
if (user == null) {
throw new UserException("User not found with email - " + email);
}
return user;
}

}

Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("/profile")
public ResponseEntity<Users> getUserProfileHandler(@RequestHeader("Authorization") String jwt) throws UserException {
Users user = userService.findUserProfileByJwt(jwt);
return new ResponseEntity<Users>(user, HttpStatus.ACCEPTED);
}

}

--

--