In this article, we will continue with our learning of spring security and will see how to implement spring security with JWT token-based authentication.
If you are new to spring security, I highly recommend you go through the basics of Spring Security first before looking at JWT implementation.
Json Web Token (JWT) 101
Json Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
JWT Structure
In the compact form, JWT consists of three parts separated by dot (.), which are:
i. Header: It typically consists of two parts the type of token which is JWT, and the signing algorithms.
ii. Payload: It consists of the claims. Claims are statements about an entity (typically, the user) and additional data.
iii. Signature: The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.
Therefore, a JWT typically looks like this
How does JWT work?
In authentication when a user logs in using their credential, a JWT is returned. Since tokens are credentials, great care must be taken to prevent security issues.
Whenever the user wants to access a protected route or resource, the user agent should send the JWT, generally in the **Authorization **header using the **Bearer **schema.
The header content should be like this:
Authorization: Bearer
The server’s protected routes will check for a valid JWT in the header, and if it is present the user will be allowed to access the protected resource.
Implementation
Let’s create a basic spring boot web application using Spring Initializer and add Spring Security and JWT dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
In this example, we will be using the H2 in-memory database to store our user credentials and fetch those credentials to authenticate. And we will add our user credentials to the database during the application start-up using @PostConstruct
annotation.
@PostConstruct
public void setup() {
userRepository.save(new UserEntity(1, "shail@mail.com", passwordEncoder.encode("shail@123"), "ROLE_ADMIN"));
userRepository.save(new UserEntity(2, "john@mail.com", passwordEncoder.encode("john@123"), "ROLE_USER"));
}
Generating Token
To generate a token user/client needs to send a request to localhost:8080/login with a username and password. It will validate the username and password using the Spring AuthenticationManager authenticate method.
If the user is valid then the Authentication object is returned otherwise it will throw InvalidUserException.
Then invoke the generateToken method to generate a valid JWT token.
Validating Token
When a user/client sends a request to a protected route then JwtTokenAuthenticationFilter
(which is a custom-implemented filter) will filter the request and will check for a token in the request.
If the request has a token, then extract the token to get the username from the token and validate if the token has not expired. If the token is valid then update the SecurityContext
with the authenticated user.
TokenHelper
TokenHelper class is responsible for JWT-related operations like generating and validating token and getting username from the token. It uses io.jswonwebtoken.Jwts for achieving this.
package com.shail.security.helper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.Objects;
import java.util.stream.Collectors;
@Component
public class TokenHelper {
private final String AUTH_HEADER_PARAM_NAME = "Authorization";
private final String AUTH_HEADER_TOKEN_PREFIX = "Bearer";
private final String SIGNING_KEY = "ASecretKeyToSigYourJWTToken";
private final Long EXPIRE_IN = 600000L;
private final String AUTH_HEADER_USERNAME = "username";
private final String AUTH_HEADER_PASSWORD = "password";
private final String AUTH_HEADER_ROLES = "roles";
// get token from http request header
public String getToken(HttpServletRequest request) {
String authToken = request.getHeader(AUTH_HEADER_PARAM_NAME);
if(Objects.isNull(authToken)) {
return null;
}
return authToken.substring(AUTH_HEADER_TOKEN_PREFIX.length());
}
//Get username from the token
public String getUsernameFromToken(String token) throws Exception {
String username = null;
try {
final Claims claims = Jwts.parser().setSigningKey(SIGNING_KEY.getBytes()).parseClaimsJws(token).getBody();
username = String.valueOf(claims.get(AUTH_HEADER_USERNAME));
}
catch (Exception e) {
throw new Exception("INVALID JWT TOKEN");
}
return username;
}
//Validate the token if it has expired or not
public boolean isValidToken(String token) throws Exception {
boolean isValid = true;
try {
final Claims claims = Jwts.parser().setSigningKey(SIGNING_KEY.getBytes()).parseClaimsJws(token).getBody();
isValid = !(claims.getExpiration().before(new Date()));
}
catch (Exception e) {
throw new Exception("INVALID JWT TOKEN");
}
return isValid;
}
// Generate token for the authenticated user
public String generateToken(String username, Collection<GrantedAuthority> authorities) {
Claims claims = Jwts.claims();
String roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
claims.put(AUTH_HEADER_USERNAME, username);
claims.put(AUTH_HEADER_ROLES, roles);
Date expiration = Date.from(Instant.ofEpochMilli(new Date().getTime()+EXPIRE_IN));
String token = Jwts.builder().setClaims(claims).setIssuedAt(new Date()).setExpiration(expiration).signWith(SignatureAlgorithm.HS256, SIGNING_KEY.getBytes()).compact();
return token;
}
}
UserDetailsServiceImpl
It implements the Spring Security UserDetailsService
interface and overrides the method loadUserByUsername
for fetching user details from the database. The Spring Security Authentication Manager calls this method for fetching the user details when authenticating the user.
package com.shail.security.service;
import com.shail.security.model.UserEntity;
import com.shail.security.repository.UserEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserEntityRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = repository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("INVALID USERNAME"));
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(user.getRole());
UserDetails userDetails = new User(user.getUsername(), user.getPassword(), authorities);
return userDetails;
}
}
JwtAuthenticationTokenFilter
This class extends the Spring Web Filter OncePerRequestFilter class. For every incoming request this Filter class gets executed and checks for a valid JWT token and if it has a valid JWT token then it sets the Authentication in SecurityContext, to specify that the current user is authenticated.
package com.shail.security.filter;
import com.shail.security.helper.TokenHelper;
import com.shail.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
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;
import java.util.Objects;
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
@Autowired
private TokenHelper tokenHelper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("Request URL: "+request.getRequestURL());
String token = tokenHelper.getToken(request);
if(!Objects.isNull(token)) {
try {
String username = tokenHelper.getUsernameFromToken(token);
if(!Objects.isNull(username) && tokenHelper.isValidToken(token)) {
UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}else {
throw new Exception("INVALID USERNAME");
}
}
catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpStatus.BAD_REQUEST.value());
}
}
filterChain.doFilter(request, response);
}
}
SecurityConfig
This class extends the WebSecurityConfigurerAdapter class and overrides the method configure (HttpSecurity).
package com.shail.security.config;
import com.shail.security.filter.JwtTokenAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager jwtAuthenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public JwtTokenAuthenticationFilter jwtTokenAuthenticationFilter () {
return new JwtTokenAuthenticationFilter();
}
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtTokenAuthenticationFilter(), BasicAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/h2-console/**", "/api/login", "/api/any/message/").permitAll()
.anyRequest().authenticated();
}
}
Testing
To test our implementation let's create a simple controller with some GetMapping which returns different messages and a PostMapping login method to generate the token.
package com.shail.security.controller;
import antlr.Token;
import com.shail.security.helper.TokenHelper;
import com.shail.security.model.UserDto;
import com.shail.security.model.UserEntity;
import com.shail.security.repository.UserEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
@RestController
@RequestMapping(value = "api")
public class MessageController {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserEntityRepository repository;
@Autowired
private TokenHelper tokenHelper;
@Autowired
private AuthenticationManager authenticationManager;
@GetMapping(value = "/admin/message")
public String adminMessage(Authentication auth) {
String role = "";
for (GrantedAuthority gauth: auth.getAuthorities()) {
role = gauth.getAuthority();
}
return "<h1>Hello, "+ auth.getName()+ " you are "+ role+"</h1>";
}
@GetMapping(value = "/user/message")
public String userMessage(Authentication auth) {
String role = "";
for (GrantedAuthority gauth: auth.getAuthorities()) {
role = gauth.getAuthority();
}
return "<h1>Hello, "+ auth.getName()+ " you are "+ role+"</h1>";
}
@GetMapping(value = "/any/message")
public String anyMessage(Authentication auth) {
String role = "";
if(auth != null) {
for (GrantedAuthority gauth : auth.getAuthorities()) {
role = gauth.getAuthority();
}
return "<h1>Hello, " + auth.getName() + " you are " + role + "</h1>";
}
return "<h1>Hello, unknown user, this url is public to access</h1>";
}
@RequestMapping(value="/login",method= RequestMethod.POST)
public ResponseEntity<String> validateLogin(@RequestBody UserDto user) throws Exception{
Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
if(authenticate.isAuthenticated()) {
User user1 = (User) authenticate.getPrincipal();
return new ResponseEntity<String>(tokenHelper.generateToken( user1.getUsername(), (Collection<GrantedAuthority>) authenticate.getAuthorities()), HttpStatus.OK);
}
else {
return new ResponseEntity<String>("INVALID USERNAME/PASSWORD", HttpStatus.BAD_REQUEST);
}
}
}
Generating Token
Send a POST request to URL "localhost:8080/api/login" with username and password in the body.
Validate Token
Try accessing the URL "localhost:8080/api/user/message" with the above generated token.
Summary
In this article, we learned how to implement Spring Security with JWT and authenticate users to access protected routes.
You can find the source code for the above implementation @ GitHub