Securing with Spring Security and JWT

 An overlook into Spring Security and securing a Spring Boot application with the use of JWT.

First, let us take a look at what JWT is.

What is JWT?

JWT stands for Java Web Token.

It is a publicly available standard or open standard (RFC (Request for Comments) 7519) which is a JSON payload object that is compact and all information that is required for the particular authentication is contained withing the compact JSON object. 

It can be used for secure communication of information between multiple parties and can be trusted because the method is digitally signed.    

With the use of the JWT, a client need only validate themselves once to the server, whereafter, a token or a Java Web Token is generated.

This does not mean that no authentication is required for subsequent calls from the server. It simply means that the client need not perform any validation with a username or password after a JWT is generated. When a token is generated, the client can then, authenticate themselves using that token.

How Does JWT Work?

1. The client sends a POST request to the server along with the username and password that is required for validation in the body of the request. This is usually done in the "/authenticate" API call.

2. The server takes the username and password and validates them. If the details are valid, the server generates a JWT which is digitally signed with a secret key, which is known only by the server and encoded using a hashing algorithm.

3. This JWT is returned to the client.

4. Any time that the client wishes to make a request, the Header of the request must contain the token as 'Authorization'.

5. The server validates the JWT that was sent in the request with the secret key and sends a response only if the JWT is valid.


Enabling Spring Security and JWT in an Existing Application

When adding Spring Security features and JWT to an existing Spring Boot application, it is simple as it requires the addition of only two dependencies. The following dependencies can be added to the pom.xml file to enable the support of Spring Security to the application;

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

Configuring JWT for Spring Application

Assuming that you have already created a Spring application, and added the above dependencies to the pom.xml file, the following classes should be added to the application in order to configure JWT.

Firstly, in a Model package, the following classes must be created;

JwtResponse

package com.springapi.demo.model;

import java.io.Serializable;
public class JwtRequest implements Serializable {
private static final long serialVersionUID = 5926468583005150707L;
private String username;
private String password;

//need default constructor for JSON Parsing
public JwtRequest()
{
}
public JwtRequest(String username, String password) {
this.setUsername(username);
this.setPassword(password);
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}

JwtRequest

package com.springapi.demo.model;

import java.io.Serializable;
public class JwtResponse implements Serializable {
private static final long serialVersionUID = -8091879091924046844L;
private final String jwtToken;
public JwtResponse(String jwtToken) {
this.jwtToken = jwtToken;
}
public String getToken() {
return this.jwtToken;
}
}
The above models will facilitate the request and response of the JWT.

Thereafter, the following classes should be created in a Configuration package.

WebSecurityConfig

package com.springapi.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders. AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

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

@Autowired
private UserDetailsService jwtUserDetailsService;

@Autowired
private JwtRequestFilter jwtRequestFilter;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}

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

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// We don't need CSRF for this example
httpSecurity.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers("/authenticate").permitAll().
// all other requests need to be authenticated
anyRequest().authenticated().and().
// make sure we use stateless session; session won't be used to
// store user's state.
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()                 .sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter         .class);
}
}
Here, in the configure method that has been overridden, there is a function called "authorizeRequests().antMatcher("/authenticate")". This specifies to the Spring Security that only requests that are called as "/authenticate" can be accessed without authorization and for all other requests, authorization is required.

This means that no other request can be processed without the JWT.

JwtRequestFilter

package com.springapi.demo.config;

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

import com.springapi.demo.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,              FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT");
} catch (ExpiredJwtException e) {
System.out.println("JWT has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() ==            null) {
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);

// if token is valid configure Spring Security to manually set
// authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =             new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource()                 .buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext()                 .setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}

JwtAuthenticationEntryPoint

package com.springapi.demo.config;

import java.io.IOException;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -7858869558953243875L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}


JwtTokenUtil

package com.springapi.demo.config;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
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 JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -2550185165626007488L;
public static final long JWT_TOKEN_VALIDITY = 60 * 30;     //Validity period set for 30 minutes     //This time can be changed as required (60*60) for one hour and so on

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

//retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
//retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
//for retrieveing any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
//check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
//generate token for user
public String generateToken(UserDetails userDetails) {
Map<String
, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
//while creating the token -
//1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
//2. Sign the JWT using the HS512 algorithm and secret key.
//3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json -web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
return "Bearer " + Jwts.builder().setClaims(claims).setSubject(subject)              .setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(
new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.
HS512, secret).compact();
} //validate token
  public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

With the JWT_TOKEN_VALIDITY variable, the validity period for the JWT can be set for as long as is preferred. Here, it is set for 30 minutes and thereafter, the token will expire and no longer provide authorization when given in the header.

After 30 minutes, an error message of "JWT has expired" as specified in the JwtRequestFilter class.

In the Controller package, add the following class;

JwtAuthenticationController

package com.springapi.demo.controller;

import com.springapi.demo.config.JwtTokenUtil;
import com.springapi.demo.model.JwtRequest;
import com.springapi.demo.model.JwtResponse;
import com.springapi.demo.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RestController
@CrossOrigin
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private JwtUserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(       @RequestBody JwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}

private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken             (username, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}

In the Services package, add the following class;

JwtUserDetailsService

package com.springapi.demo.service;

import java.util.ArrayList;

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;

@Service
public class JwtUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("secret".equals(username)) {
return new User("secret",             "$2y$12$1v/JEIYfJJJaBUrf.Jqjvu18lzMbcmlHQCboVvlyWGhZ4Cf4WP8cK",
new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}

UserDetailsService is an interface that is provided by Spring Security in order to aid in authorization. It has one method which has been overridden here.

In this example, the username is hard coded as "secret" and the password, which is encoded with BCrypt encryption, is also "secret". These credentials are necessary for initial authentication.

These details can be changed as per your liking. 

If the password needs to be changed, the BCrypt encryption should be added in its place.

If the username needs to be changed, it should be changed in the above class, as well as in application.properties.

The final change that needs to be made is that the following needs to be specified in the application.properties file.

#Jwt Properties
jwt.secret = secret
 
If the username in the application.properties file and the JwtUserDetailsService class are different, the authentication will fail.

After all the above changes are made to the application, it can be tested in Postman or any other endpoint testing application.

The first request that needs to be sent is the /authenticate request which will generate the required token. 

When sending this request, the body of the request should contain the username and password specified in the application.


When this is run, the following response should be received:


Thereafter, if any request is to be made in this application, the token should be added to the headers in that request under "Authorization".


It is only then that the request would get a response. If not, you will receive an error message that says "Access Forbidden".

This is how to add basic JWT Authorization through Spring Security to an application. 

The credentials in this case are hard coded and can be modified to be retrieved from a database.

For now, this is how Spring Security works.

Happy Securing!







Comments

Popular Posts