Securing your backend using JWT
In the previous section, we covered how to use basic authentication with the RESTful web service. That is not usable when we are going to develop our own frontend with React. We are going to use the JSON Web Tokens (JWT) authentication in our application. JWT is a compact way to implement authentication in modern web applications. JWT is really small in size and therefore it can be sent in the URL, in the POST parameter, or inside the header. It also contains all required information about the user.
The JSON web token contains three different parts separated by dots. The first part is the header that defines the type of the token and the hashing algorithm. The second part is the payload that, typically, in the case of authentication, contains information about the user. The third part is the signature that is used to verify that the token hasn't been changed along the way. You can see the following example of a JWT token:
eyJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJKb2UifD.
ipevRNuRP6HflG8cFKnmUPtypruRC4fc1DWtoLL62SY
The following diagram shows the main idea of the JWT authentication process:
After successful authentication, the requests sent by the user should always contain the JWT token that was received in the authentication.
We will use the Java JWT library (https://github.com/jwtk/jjwt), which is the JSON Web Token library for Java and Android; therefore, we have to add the following dependency to the pom.xml file. The JWT library is used for creating and parsing JWT tokens:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
The following steps show how to enable JWT authentication in our backend:
- Create a new class called AuthenticationService in the service package. In the beginning of the class we will define a few constants; EXPIRATIONTIME defines the expiration time of the token in milliseconds. SIGNINGKEY is an algorithm-specific signing key used to digitally sign the JWT. You should use a base64 encoded string. PREFIX defines the prefix of the token and the Bearer schema is typically used. The addToken method creates the token and adds it to the request's Authorization header. The signing key is encoded using the SHA-512 algorithm. The method also adds Access-Control-Expose-Headers to the header with the Authorization value. This is needed because we are not able to access the Authorization header through a JavaScript frontend by default. The getAuthentication method gets the token from the response Authorization header using the parser() method provided by the jjwt library. The whole AuthenticationService source code can be seen here:
package com.packt.cardatabase.service;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import static java.util.Collections.emptyList;
public class AuthenticationService {
static final long EXPIRATIONTIME = 864_000_00; // 1 day in milliseconds
static final String SIGNINGKEY = "SecretKey";
static final String PREFIX = "Bearer";
// Add token to Authorization header
static public void addToken(HttpServletResponse res, String username) {
String JwtToken = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis()
+ EXPIRATIONTIME))
.signWith(SignatureAlgorithm.HS512, SIGNINGKEY)
.compact();
res.addHeader("Authorization", PREFIX + " " + JwtToken);
res.addHeader("Access-Control-Expose-Headers", "Authorization");
}
// Get token from Authorization header
static public Authentication getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
String user = Jwts.parser()
.setSigningKey(SIGNINGKEY)
.parseClaimsJws(token.replace(PREFIX, ""))
.getBody()
.getSubject();
if (user != null)
return new UsernamePasswordAuthenticationToken(user, null,
emptyList());
}
return null;
}
}
- Next, we will add a new simple POJO class to keep credentials for authentication. Create a new class called AccountCredentials in the domain package. The class has two fields—username and password. The following is the source code of the class. This class doesn't have the @Entity annotation because we don't have to save credentials to the database:
package com.packt.cardatabase.domain;
public class AccountCredentials {
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;
}
}
- We will use filter classes for login and authentication. Create a new class called LoginFilter in the root package that handles POST requests to the /login endpoint. The LoginFilter class extends the Spring Security AbstractAuthenticationProcessingFilter, which requires that you set the authenticationManager property. Authentication is performed by the attemptAuthentication method. If the authentication is successful, the succesfulAuthentication method is executed. This method will then call the addToken method in our service class and the token will be added to the Authorization header:
package com.packt.cardatabase;
import java.io.IOException;
import java.util.Collections;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.packt.cardatabase.domain.AccountCredentials;
import com.packt.cardatabase.service.AuthenticationService;
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
public LoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException {
AccountCredentials creds = new ObjectMapper()
.readValue(req.getInputStream(), AccountCredentials.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
Collections.emptyList()
)
);
}
@Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
AuthenticationService.addToken(res, auth.getName());
}
}
- Create a new class called AuthenticationFilter in the root package. The class extends GenericFilterBean, which is a generic superclass for any type of filter. This class will handle authentication in all other endpoints except /login. The AuthenticationFilter uses the addAuthentication method from our service class to get a token from the request Authorization header:
package com.packt.cardatabase;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import com.packt.cardatabase.service.AuthenticationService;
public class AuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest)request);
SecurityContextHolder.getContext().
setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
- Finally, we have to make changes to our SecurityConfig class's configure method. There, we define that the POST method request to the /login endpoint is allowed without authentication and that requests to all other endpoints need authentication. We also define the filters to be used in the /login and other endpoints by using the addFilterBefore method:
//SecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and()
// Filter for the api/login requests
.addFilterBefore(new LoginFilter("/login",
authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
// Filter for other requests to check JWT in header
.addFilterBefore(new AuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
- We will also add a CORS (Cross-Origin Resource Sharing) filter in our security configuration class. This is needed for the frontend, that is sending requests from the other origin. The CORS filter intercepts requests, and if these are identified as cross origin, it adds proper headers to the request. For that, we will use Spring Security's CorsConfigurationSource interface. In this example, we will allow all HTTP methods and headers. You can define the list of allowed origins, methods, and headers here, if you need more finely graded definition. Add the following source into your SecurityConfig class to enable the CORS filter:
// SecurityConfig.java
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
Now, after you run the application, we can call the /login endpoint with the POST method and, in the case of a successful login, we will receive a JWT token in the Authorization header:
After successful login, we can call the other RESTful service endpoints by sending the JWT token received from the login in the Authorization header. See the example in the following screenshot:
Now, all the functionalities that are needed have been implemented to our backend. Next, we will continue with backend unit testing.