Secure REST End Points with Spring Security & JWT(JASON Web Token)

Amila Iroshan
The Fresh Writes
Published in
11 min readFeb 14, 2023

Introduction

In this article I’ll explain how to secure REST API using spring security and JWT.

Pre requisite,

Basic knowledge of configure spring security with spring boot application. I have already written two articles which express how to handle authentication and authorization using spring security from scratch to advanced.
Please refer this articles :

  1. Spring Security 1
  2. Spring Security 2

Why we need JWT ?

As I described on my previous articles I have configured spring security with server side web applications which web pages are render from server side. Those security things and communication based on sessions.
This Spring Security with JWT application provides authentication/authorization and configure it for stateless communication.

Let’s get start

What is JWT(JASON Web Token) ?

In breif this is a way of secure data transferring in between client and server via internet. JWT uses token based architecture to achieve it and it follows open internet standards.
This token digitally signed with a private secret or public/private key using a cryptographic algorithm. So no one can stole the data without private key. The sample JASON web token looks like this and it contains three blocks and it separated by “.”

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJURVNUIiwiaXNzIjoiQW1pbGEiLCJpYXQiOjE2NzYyOTcxMjMsImVtYWlsIjoidnBhaXBha2kifQ.L0_kotSGAFH9hx_63_Xr9U7GzD_Nai3FwnqM_vvfR04

These three blocks called as Header, Payload and Signature. You can decode your jwt token and see the containing information using https://jwt.io/

Decode JWT

The first part of the token called “Header” and it contains the token type and algorithm which is used for signing.
Then the second part of jwt known as payload and its data called as a claims.The claims consists diffrent kind of data which we consent to share.
Important : Do not add some secret/confidential data to claim when generating the jwt. Like passwords.
The signature block consists encoded string and it is build by digital sign of header and payload with cryptographic algorithm along secret key.

JWT Authentication Flow

Authentication Flow

Lets begin implementation

In this project I’m using technology stack as bellow

Java 17
Spring Boot with Spring Security, Spring Web and Spring Data JPA
jjwt 3.18.2
MySQL
Maven as build tool
Postman as rest client

Create spring boot application.

- Navigate to https://start.spring.io.

- Choose either Gradle or Maven as build tool. In here I’m using mavean, Java 18 and .jar as packaging.

Here I’ve added following dependencies to my project,

Spring Web — contains common web-specific utilities with Servlet
Spring Data JPA — bind data with JPA.
Spring Security — provide spring security configuration .
Java JWT — Java implementation of JSON web tokens. (JWT)
MySQL Driver — Driver for access MySQL based database.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jwt</groupId>
<artifactId>Spring_Security_JWT</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring_Security_JWT</name>
<description>Sample project JWT with Spring Security</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>


</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

My Project Structure

Project Structure

Exposed Rest End Points

Here I have exposed three end points and those are as follw,
Http POST apis
/auth/signup — Creates and persists an User entity object with data validation.
/auth/signin — Authenticates user credentials and generates a JWT
Http GET api
/user/userinfo — Respond user information for the authenticated users only.

1).Create Entity Classes.

In this project I’m using spring data jpa and mysql connector to deal with MySQL dbms. Here is my application.properties file for db connection.

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/jwt_security
spring.datasource.username=root
spring.datasource.password=amila
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

NOTE : My application has two roles which are ADMIN role and USER role.I have used enum class for define these two roles.Besides that it has three data tables called User,Roles and user_roles which has
many to many relationship.

public enum UserRoles {
ROLE_USER,
ROLE_ADMIN;
public class RoleNames{
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
@Entity 
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable( name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();

@Override
public String toString() {
return "User [id=" + id + ", email=" + username + ", password=" + password + "]";
}

public User() {
super();
}

public Long getId() {
return id;
}

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

public String getUsername() {
return username;
}

public void setUsername(String email) {
this.username = email;
}

public String getPassword() {
return password;
}

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

public Set<Role> getRoles() {
return roles;
}

public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}

@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Enumerated(EnumType.STRING)
private UserRoles name;

public Role() {

}

public Role(UserRoles name) {
this.name = name;
}

public Integer getId() {
return id;
}

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

public UserRoles getName() {
return name;
}

public void setName(UserRoles name) {
this.name = name;
}
}

2).Create repository Classes.

Each entity class need separate repository classes when deal with data jpa. Here I’m extending JpaRepository interfaces with my repository classes.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
public Optional<User> findByUsername(String username);
}
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(UserRoles name);
}

3).Create DTO (Data transfer Object) classes for SignIn and SignUp requests.

public class SignUpRequest {

private String username;
private String password;
private Set<String> role;

public SignUpRequest() {
super();
}
public SignUpRequest(String username, String password) {
super();
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<String> getRole() {
return role;
}
public void setRole(Set<String> role) {
this.role = role;
}
}
public class SignInRequest {

private String username;
private String password;

public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public SignInRequest() {
super();
}
public SignInRequest(String username, String password) {
super();
this.username = username;
this.password = password;
}
}

4).Configure Spring Security with application.

Note: WebSecurityConfigurerAdapter is depreciated and it’s under maintainance mode since spring 2.7.0.So you can use WebSecurityConfig with latest spring projects.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class JWTSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private UserRepository userRepository;
@Autowired
private JWTFilterConfig filterConfig;
@Autowired
private UserDetailsServiceImplement detailsServiceImplement;

@Override
protected void configure(HttpSecurity http) throws Exception { // Method to configure your app security
http.csrf().disable()
.httpBasic().disable()
.cors()
.and()
.authorizeHttpRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated().and()
.userDetailsService(detailsServiceImplement)
.exceptionHandling()
.authenticationEntryPoint(
(request, response, authException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(filterConfig, UsernamePasswordAuthenticationFilter.class);
}

// bean for the BCryptPasswordEncoder password encoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// bean of the authentication manager which will be used to run the authentication process
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

By extending WebSecurityConfigurerAdapter class and use with @EnableWebSecurity annotation we can allow web security quickly with our app. Then override the configure(HttpSecurity http) method from WebSecurityConfigurerAdapter we can tells Spring Security
how we configure CORS ,CSRF as well as we want to require all users to be authenticated or not.
Then we have to provide userdetailservice implementation which is responsible for load User details to perform authentication & authorization. So it has UserDetailsService interface that we need to implement.
The implementation of UserDetailsService will be used for extract user identity information based on credentials from a database and then perform validation. LoadUserByUsername method accepts username as a parameter and returns the user identity object.
In addition to that we have to create bean of PasswordEncoder.

Please note that we added the JWTFilterConfig before the Spring Security internal UsernamePasswordAuthenticationFilter.
We’re doing this because we need access to the user identity at this point to perform authentication/authorization, and its extraction happens inside the JWT token filter based on the provided JWT token.

There are mainly two ways of specify the authorization in security config file.
1).URL-based configuration
2).Annotation-based configuration

.antMatchers("/user/**").hasRole(UserRoles.USER)
.antMatchers("/api/author/**").hasRole(UserRoles.ADMIN)

The URL-based configuration code snippet attached with here and this approach is simple and straightforward.
But timely our application growing rapidly and If our application contains much user roles ,this kind of user role configuration getting complex and hard to read.

So I personely prefer Annotation-based configuration and by adding below annotation we can enable role base autherization.
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)

securedEnabled = true enables @Secured annotation.
jsr250Enabled = true enables @RolesAllowed annotation.
prePostEnabled = true enables @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter annotations.

5).Implement UserDetails & UserDetailsService

UserDetailService is the class which coming from spring security which we could use to introduce implementation on how our application should read a user. In here I’m implementing UserDetailsService and override loadByUsername method.
Then we can add our custom implementation on how Spring security should read the user.

Then we should set our user’s username and password with role list to the org.springframework.security.core.userdetails.User.

@Component
public class UserDetailsServiceImplement implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Optional<User> user = userRepository.findByUsername(username);
if (!user.isPresent()) {
throw new UsernameNotFoundException("User not found!");
}
return UserDetailsServices.build(user.get());
}
}

6).Filter request with JWTFilterConfig

After config OncePerRequestFilter class with our application it does identify and validate the given token of any incoming request.
Then override the doFilterInternal method which is capable of capturing incoming requests and allow or block the request after validating it

Let’s define a filter that executes once per request. So we create JWTFilterConfig class that extends OncePerRequestFilter and override doFilterInternal() method.

@Component
public class JWTFilterConfig extends OncePerRequestFilter {

@Autowired
private UserDetailsServiceImplement detailsServiceImplement ;
@Autowired
private JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String authHeader = request.getHeader("Authorization");

// Checking if the header contains a Bearer token
if(authHeader != null && !authHeader.isBlank() && authHeader.startsWith("Bearer ")){
// Extract JWT
String jwt = authHeader.substring(7);
if(jwt == null || jwt.isBlank()){
// Invalid JWT
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT Token in Bearer Header");
}else {
try{
// Verify token and extract email
String email = jwtUtil.validateTokenAndRetrieveSubject(jwt);

// Fetch User Details
UserDetails userDetails = detailsServiceImplement.loadUserByUsername(email);

// Create Authentication Token
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(email, userDetails.getPassword(), userDetails.getAuthorities());

// Setting the authentication on the Security Context using the created token
if(SecurityContextHolder.getContext().getAuthentication() == null){
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}catch(JWTVerificationException exc){
// Failed to verify JWT
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT Token");
}
}
}
// Continuing the execution of the filter chain
filterChain.doFilter(request, response);
}

7).Create JWT Utility class

The responsibility of this class is
1).Generate a JWT from username, date, expiration, secret
2).Get username from JWT
3).Validate a JWT

@Component
public class JWTUtil {
private String secret=AuthenticationConfigConstants.SECRET;
public String generateToken(String username) throws IllegalArgumentException, JWTCreationException {
try {
return JWT.create()
.withSubject(AuthenticationConfigConstants.SUBJECT)
.withClaim("username", username)
.withIssuedAt(new Date())
.withIssuer(AuthenticationConfigConstants.ISSUER_NAME)
.sign(Algorithm.HMAC256(secret));
} catch (IllegalArgumentException e) {
return e.getMessage();
}
catch (JWTCreationException e) {
return e.getMessage();
}
}
public String validateTokenAndRetrieveSubject(String token)throws JWTVerificationException {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))
.withSubject(AuthenticationConfigConstants.SUBJECT)
.withIssuer(AuthenticationConfigConstants.ISSUER_NAME)
.build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaim("email").asString();
}
}

8).Create AuthenticationConfigConstants

This class contain the constant values like jwt secret key and etc..

public class AuthenticationConfigConstants {
public static final String SECRET = "Amila_JWT";
public static final String SUBJECT = "TEST";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
public static final String ISSUER_NAME = "Amila";
}

9).Handle Exception

Here I’m using controller advicer for handle exceptions across the whole application in one global handling component.It helps to extract the exception handling from controller classes and mitigate the code lines form controller classes.

@ControllerAdvice
public class ControllerAdvisor extends ResponseEntityExceptionHandler {

@ExceptionHandler(InvalidCredentialException.class)
public ResponseEntity<Object> handleInvalidCredentialException(
InvalidCredentialException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", "Invalid Credentials(username and password required)");
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(UserAlreadyExistsException.class)
public ResponseEntity<Object> handleUserAlreadyExistsException(
UserAlreadyExistsException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", "User already registered. Please use different username.");
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}


@ExceptionHandler({IllegalArgumentException.class,JWTCreationException.class})
public ResponseEntity<Object> handleIllegalArgumentAndJWTCreationExceptionException(
Exception ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDate.now());
body.put("status", status.value());
body.put("errors", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler({AuthenticationException.class})
public ResponseEntity<Object> handleAuthenticationException(
AuthenticationException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDate.now());
body.put("status", status.value());
body.put("errors", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}

10). Create Rest Controllers

The AuthenticationController class has two end points for user signup and signin purposes.
– /auth/signup

check existing username
create new User with ROLE_USER
save User to database using UserRepository

– /auth/signin

authenticate { username, password }
update SecurityContext using Authentication object
generate JWT
get UserDetails from Authentication object
response contains JWT and UserDetails data

@RestController 
@RequestMapping("/auth")
public class AuthenticationController {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private AuthenticationManager authManager;
@Autowired
private UserService userService;
@PostMapping("/signup")
public ResponseEntity<Map<String, Object>> signUpHandler(@RequestBody SignUpRequest loginDetails){

// Save User Entity on Database
userService.createUser(loginDetails);

Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.CREATED);
body.put("message", "User registered successfully!");
return new ResponseEntity<>(body, HttpStatus.CREATED);
}

@PostMapping("/signin")
public ResponseEntity<Map<String, Object>> signinHandler(@RequestBody SignUpRequest signInRequest){

Authentication authentication = authManager.authenticate(
new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword()));

SecurityContextHolder.getContext().setAuthentication(authentication);

UserDetailsServices userDetails = (UserDetailsServices) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());

String token = jwtUtil.generateToken(signInRequest.getUsername());
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("timestamp", LocalDateTime.now());
payload.put("user_name", userDetails.getUsername());
payload.put("roles", roles);
payload.put("jwt-token", token);

return new ResponseEntity<>(payload, HttpStatus.CREATED);

}

}

11). Run & Test

Run Spring Boot application. Tables are automatically generated by Java persistence api and then you need to manually insert the master data to roles table.

INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');

Then you can check it with posman rest client application.

Signup
With wrong token

Thank you for read this article and If you liked the article, do follow and clap 👏🏻.Happy coding, Cheers !!

You can find the complete code for this example on GitHub

--

--

Amila Iroshan
The Fresh Writes

Software Engineer | Open Source Contributor | Tech Enthusiast