Today I will show you how to make your REST APIs secure and authenticate requests using Spring Security JWT (Json Web Token).
Today, JWT is one of the most common methods for authenticating requests for Java WEB applications. And this is not casual. This type of protection and authentication has several advantages:
- convenience (you do not need to transfer your username and password with each request);
- fewer requests to the database (the token can contain basic information about the user);
- ease of implementation (just use a ready-made library to generate and decrypt a token)
Basically, a token is just a string that is generated at the request of a user who wants to call protected resources in the future. The user registers in the system, then makes a request to generate a token. Then he can make authorized requests to the server using the token. Typically, the token is placed in the request header for ease of transmission and reading.
When a token is generated on the server, data on the user’s session or other necessary information is placed into it, if necessary, and encrypted using encryption algorithms and a secret key. After all, then the token will need to be decrypted when the user makes a request. The secret key must be well protected. After all, you do not need the data from the token to be obtained by attackers or other persons.
Now let’s move on to an example. For this we need a simple Spring Boot application with REST API, which we will try to obfuscate using jwt and Spring Security.
In order to be able to use Spring Security, you need to connect the dependency to our project:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
After that, the functionality of the library is available to me. First, I create a generateToken method, the input of which will receive the user’s login and the output will be the jwt line:
public String generateToken(String login) {
Date date = Date.from(LocalDate.now().plusDays(15).atStartOfDay(ZoneId.systemDefault()).toInstant());
return Jwts.builder()
.setSubject(login)
.setExpiration(date)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
As you can see in the example above, I use the Jwts.builder () construction that allows you to create this very token. I added the user’s login to setSubject, so that later it can be taken from there in the filter when the user makes a request. setExpiration – I specified 15 days. If 15 days pass and the token is not renewed, an error message will be thrown in the validateToken method, which will be described below. signWith – accepts a signature algorithm and a codeword as input, which will then be required for decryption. I made a jwtSecret field in the config file and put it in the class using
@Value("$(jwt.secret)")
private String jwtSecret;
constructions. My settings file (application.properties) looks like this:
server.port=8087
spring.datasource.url=jdbc:postgresql://localhost:5432/spring_security_jwt
spring.datasource.username=postgres
spring.datasource.password=postgres
jwt.secret=javamaster
Of course, it is not a good idea to store the decryption and encryption key of the token in the configuration file. Especially if you value the safety of your users. This key, like database passwords, is usually stored in special storages. For example AWS Secret Manager.
The validateToken method I wrote about above looks like this:
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (ExpiredJwtException expEx) {
log.severe("Token expired");
} catch (UnsupportedJwtException unsEx) {
log.severe("Unsupported jwt");
} catch (MalformedJwtException mjEx) {
log.severe("Malformed jwt");
} catch (SignatureException sEx) {
log.severe("Invalid signature");
} catch (Exception e) {
log.severe("invalid token");
}
return false;
}
The parseClaimsJws method can throw very detailed exception types that you can handle appropriately. I log an error message and return false if the validation failed.
To get information about the user’s login, I wrote the getLoginFromToken method:
public String getLoginFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
When generating a token, I put a token in the Subject – it means that if the token is valid, it will have a login. Full view of my JwtProvider class:
package com.javamaster.springsecurityjwt.config.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
@Component
@Log
public class JwtProvider {
@Value("$(jwt.secret)")
private String jwtSecret;
public String generateToken(String login) {
Date date = Date.from(LocalDate.now().plusDays(15).atStartOfDay(ZoneId.systemDefault()).toInstant());
return Jwts.builder()
.setSubject(login)
.setExpiration(date)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (ExpiredJwtException expEx) {
log.severe("Token expired");
} catch (UnsupportedJwtException unsEx) {
log.severe("Unsupported jwt");
} catch (MalformedJwtException mjEx) {
log.severe("Malformed jwt");
} catch (SignatureException sEx) {
log.severe("Invalid signature");
} catch (Exception e) {
log.severe("invalid token");
}
return false;
}
public String getLoginFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
After I’m done configuring roles, permissions, and tokens, I proceed to implement the UserDetails and UserDetailsService interfaces from the org.springframework.security.core.userdetails package.
What is it for? As I described above, I need to find a user in the database and put him and his role in the Spring Security context. But I can’t put any object. Spring will only understand objects from “its own circle”. Therefore, I will need to get my user out of the database and “overtake” it into an object that is suitable for Spring Security. And this is UserDetails. UserDetailsService will serve as a helper class for this purpose.
I am creating CustomUserDetails which I implement from UserDetails. I will remake all methods that need to be implemented by default for my user. Next, I add the login and password fields to this class. I add the GrantedAuthority collection field – this is the interface for user accesses. One of its implementations is SimpleGrantedAuthority to which you can add only the role name and Spring will give access if the role name matches the role name in the hasRole method (code above). Only here you need to be careful. If you go to the implementation of hasRole, you can see that spring adds the ROLE_ prefix to the role name. Therefore, when you specify for example .antMatchers (“/ admin / *”). HasRole (“ADMIN”) you must pass the role name with the ROLE_ prefix to SimpleGrantedAuthority. For example, I made user roles initially with the ROLE_ prefix so as not to add later when passing the role name to the SimpleGrantedAuthority.
Also in the CustomUserDetails class, I added a method that converts my user from the database to a CustomUserDetails object. As a result of all the manipulations, my code has become:
package com.javamaster.springsecurityjwt.config;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class CustomUserDetails implements UserDetails {
private String login;
private String password;
private Collection<? extends GrantedAuthority> grantedAuthorities;
public static CustomUserDetails fromUserEntityToCustomUserDetails(UserEntity userEntity) {
CustomUserDetails c = new CustomUserDetails();
c.login = userEntity.getLogin();
c.password = userEntity.getPassword();
c.grantedAuthorities = Collections.singletonList(new SimpleGrantedAuthority(userEntity.getRoleEntity().getName()));
return c;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Of course, you can customize this class to suit your needs. For example, add a field to the database to the users table, which will mark whether the user is active or not. And then pass this field to the isAccountNonLocked method. But I limited myself only to the username and password and therefore changed it so that all the methods return that everything is in order with my user.
Next, I create a CustomUserDetailsService class that will implement the UserDetailsService interface. This interface has only one loadUserByUsername method. In this method, I get my user from the database by login, convert it to CustomUser and return:
package com.javamaster.springsecurityjwt.config;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import com.javamaster.springsecurityjwt.service.UserService;
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.Component;
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userService.findByLogin(username);
return CustomUserDetails.fromUserEntityToCustomUserDetails(userEntity);
}
}
Let’s start with the filter. I made a regular class that inherited from GenericFilterBean. Basically, GenericFilterBean is just a basic implementation of javax.servlet.Filter that you probably used to filter servlet requests.
After inheritance, we have one method available for the definition: doFilter.
As you might have guessed, this is the method that will work when the filter is running.
The first thing I need to do is get the token from the request. I expect the token to arrive in the header with the “Authorization” key. I can get the value of the header from the request. After extracting the token, I need to check that it starts with the word Bearer (native to English). Why with Bearer? Such standard RFC6750 for tokens with which you can find more details at the link.
Then I extract the information I need from the token. Initially, I will encrypt the username into it, so that later I can remove the username from the token and find the user in the database.
If the user is found, I put it in the Spring Security context so that the user is checked for access and can continue to work with the request. My filter code looks like this:
package com.javamaster.springsecurityjwt.config.jwt;
import com.javamaster.springsecurityjwt.config.CustomUserDetails;
import com.javamaster.springsecurityjwt.config.CustomUserDetailsService;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.springframework.util.StringUtils.hasText;
@Component
@Log
public class JwtFilter extends GenericFilterBean {
public static final String AUTHORIZATION = "Authorization";
@Autowired
private JwtProvider jwtProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("do filter...");
String token = getTokenFromRequest((HttpServletRequest) servletRequest);
if (token != null && jwtProvider.validateToken(token)) {
String userLogin = jwtProvider.getLoginFromToken(token);
CustomUserDetails customUserDetails = customUserDetailsService.loadUserByUsername(userLogin);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearer = request.getHeader(AUTHORIZATION);
if (hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
In lines
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
I create a UsernamePasswordAuthenticationToken object from the spring security library and then put this object in the SecurityContextHolder.
That’s all you need to do in order to configure Spring Security with the jwt token. It remains only to add a controller in which there will be 2 methods: registration and authorization – generating a token for the user.
package com.javamaster.springsecurityjwt.controller;
import com.javamaster.springsecurityjwt.config.jwt.JwtProvider;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import com.javamaster.springsecurityjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtProvider jwtProvider;
@PostMapping("/register")
public String registerUser(@RequestBody @Valid RegistrationRequest registrationRequest) {
UserEntity u = new UserEntity();
u.setPassword(registrationRequest.getPassword());
u.setLogin(registrationRequest.getLogin());
userService.saveUser(u);
return "OK";
}
@PostMapping("/auth")
public AuthResponse auth(@RequestBody AuthRequest request) {
UserEntity userEntity = userService.findByLoginAndPassword(request.getLogin(), request.getPassword());
String token = jwtProvider.generateToken(userEntity.getLogin());
return new AuthResponse(token);
}
}
In registration, we accept the username and password at the entrance and call the saveUser method from the UserService class.
In authorization, we first find the user by login and password. If such a user is found, we generate a token for him and return it. In this example, I did not complicate the logic of these methods. Optionally, you can add exceptions and additional validation. This will of course need to be added if you decide to use this example beyond just a Spring Security test case.
Now, all that remains is to launch the application and see how it works. To test my REST API, I will use Postman tool.
The first step is to register a user:
Next – authorization:
Here we got a token with which we can make requests to the server. Remember that we keep the user with the USER role. Therefore, first we will try to call the URL that is available for the corresponding role. Don’t forget to copy the received token and paste it into the authorization:
If we try to make a request with this token on the url that is available only to users with the ADMIN role, we will receive an error:
Our requests are now protected from unauthorized access.