DEV Community

Cover image for How to implement JWT authentication in Java with Spring Framework
EddieSCJ
EddieSCJ

Posted on • Edited on

3 2

How to implement JWT authentication in Java with Spring Framework

First of all, let's import some dependencies

Before starting the real implementation, please, try to get these dependencies on your project:

    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
    compile group: 'com.auth0', name: 'java-jwt', version: '3.10.3'
    compile group: 'org.springframework.security', name: 'spring-security-core', version: '5.1.5.RELEASE'
    compile group: 'org.springframework.security', name: 'spring-security-web', version: '5.1.5.RELEASE'
    compile group: 'org.springframework.security', name: 'spring-security-config', version: '5.1.5.RELEASE'
Enter fullscreen mode Exit fullscreen mode

You might have the basic packages to build an API like Spring Starter Web, if you don't know how to build an API with java and Spring Boot, please, read the following article: Building a Simple API with Java and Spring Boot

And make sure you already have your UserRepository Implemented, but if you don't know how to implement a simple connection between java and any SQL database with H2, please, read the following article: Implementing a Simple Database with Java, JPA, Hibernate and SQL

Creating a Bean to our PasswordEncoder

If you don't know what is a Bean, please, read the following article: What is a Java @Bean?

Please, at your main class, paste the following code:

@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder(); 
}
Enter fullscreen mode Exit fullscreen mode

It'll provide a BCryptPasswordEncoder instance to us.

Implemeneting our own UserDetailsService

So, assuming that we'll authenticate with a username and password, we have to implement the default class and method to search it in the database, right?

Follow bellow the code, please, stay alert to read the comments.

import com.bedigital.application.domain.ApplicationUser;
import com.bedigital.application.repositories.ApplicationUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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 static java.util.Collections.emptyList;
// This service will be useful to us later, so first implement the
// UserDetailsService contract and let's implement its methods.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
// Remember I said that we would need an
// application user linked to the database
private ApplicationUserRepository applicationUserRepository;
// The default method permit us to search the user by username in the database
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ApplicationUser user = applicationUserRepository.findByUsername(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
//We will wrap our credentials in the default java User Object
return new User(user.getUsername(), user.getPassword(), emptyList());
}
}

Let's implement the JWTAuthenticationFilter

First of all, our authentication will be a basic auth, where you provide a username and password and the system will verify if you are who you are supposed to be.

This class will rewrite some methods in a personal way to implement our UsernameAndPasswordAuthenticationFilter, which provides somethings like the answer to our auth.

Remember to be alert for the comments.
Follow below the code:

import com.auth0.jwt.JWT;
import com.bedigital.application.domain.ApplicationUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import static com.auth0.jwt.algorithms.Algorithm.HMAC512;
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) {
//Do you remember when I said you should have already implemented
// a class where is your user with a username and password already
// linked to the database? So, we will use an instance of this
// class here.
try {
// As you can imagine here we get the body of the request
// (provided by req.getInputStream) and Instantiate our
// ApplicationUser (that has a username and password, something
// will be provided by the login post
ApplicationUser creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
// Now we will authenticate with a default method in our authManager
// that we will override some methods later to implement a specific authentication method
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Let's reimplement the result of our authentication when successful
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException {
// To access our system we'll use a JWT (Json Web Token) that will provide
// to us some data like time expiration (you can use it to define how long
// the client can use your system with that token, furthemore we'll have
// data like, password encrypted, username and a payload (like a body) if you need
// See the SecurityConstants ? It's a class that I created, but if you wanna
// put it directly to the code, feel yourself free.
// public static final long EXPIRATION_TIME = 480_000; // milliseconds
// public static final String HEADER_STRING = "Authorization";
// public static final String SECRET = "oculos_apenas_99_reais";
// public static final String TOKEN_PREFIX = "Bearer "; // I added a space after the prefix purposefully
// public static final String SIGN_UP_URL = "/sign-up";
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.sign(HMAC512(SecurityConstants.SECRET.getBytes()));
//We could finish our application here and you would get the token in your header
res.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
String body = SecurityConstants.TOKEN_PREFIX + token;
//But I would like to return it in our response body
res.getWriter().write(body);
res.getWriter().flush();
}
}

That is our AuthenticationFilter, responsible to verify the username and password data (we can say that it is the class that execute the "login")

Lets Implement the JWTAuthorizationFilter

If the Authentication Filter verifies and confirms the data, our Authorization Filter is responsible for the request, just like verify our token and show the authorities.

Remember to be alert for the comments.
Follow Below the file:

package com.bedigital.application.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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.ArrayList;
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
// Here we substitute the default method to implement our own, with our validations and also call the authenticaiton method
// based in the type os authentication
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(SecurityConstants.HEADER_STRING);
if (header == null || !header.startsWith(SecurityConstants.TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication); // setting the auth based on our method
chain.doFilter(req, res);
}
// Reads the JWT from the Authorization header, and then uses JWT to validate the token
// At the request we received our token (after login) and in every request we might put this
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(SecurityConstants.HEADER_STRING);
if (token != null) {
// Here it is parsing the token and verifying
String user = JWT.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes())) //Here we decode the JWT received and
.build()
.verify(token.replace(SecurityConstants.TOKEN_PREFIX, "")) // now we need to replace the "BEARER" and verify
// the token alone by verify()
.getSubject(); // and finally, here is the result
if (user != null) {
// If this is positive, we return an Object with the resullt
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}

Finally, the WebSecurity Class

This is the class that interacts with the web layer, here we also have the cors configuration, allows, signup redirect, we define the login endpoint, etc etc etc.

Remember to be alert for the comments.
Follow below de the file:

package com.bedigital.application.security;
import com.bedigital.application.services.security.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
@Configuration
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder; // do you remember we created a bean for that? here is the usage
@Override
//here are some configurations, let's explain
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin() // we accpet all frames that are of the same origin (I recommend you disable
// the frameoptions
.and()
.cors().configurationSource(corsConfigurationSource()) // you can scroll down and read the cors config
.and()
.csrf().disable() // we are disabling the csrf
// (Because we used JWT, and it's a type of token with something with cookies)
.authorizeRequests()
.antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll() // we are liberating the sign up url
// to post opreations without be logged in
.antMatchers("/h2-console/**").permitAll() // here we also took as free the h2 url, because sometimes we need to verify
// the data
.anyRequest().authenticated() // here we say that any other request need to be authenticated
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager())) // our filters are here
.addFilter(new JWTAuthorizationFilter(authenticationManager())) // and our auth
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // we dont work with sessions, just jwt
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder); // here we show the class who will get
// the user info, and the encoder
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
source.registerCorsConfiguration("/**", corsConfiguration); // we permit any url after / execute http operations with our system
return source;
}
}

We can just finish here, but, as a bonus, there is a test class to test your login functionality.

#Bonus - Test Impl

package com.bedigital.application.resources;
import com.bedigital.application.domain.ApplicationUser;
import com.bedigital.application.repositories.ApplicationUserRepository;
import com.bedigital.application.services.ApplicationUserService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationTest {
private static final ApplicationUser APPLICATION_USER = new ApplicationUser("eddie", "1234");
private static final Gson GSON = new GsonBuilder().create();
@Autowired
private ApplicationUserService applicationUserService;
@Autowired
private MockMvc mockMvc;
@BeforeEach // Usually the Junit executes a rollback (undo the done operations), then, because of that we insert an user after
// each operation, IT'S WRONG, you should insert as a BeforeClass and write a TestInstance(PER_CLASS) annotation upon the class
// but I don't want to refactor this test, if you add more than one method it will break so easily as it can
public void insertData() {
applicationUserService.save(APPLICATION_USER);
}
@Test
public void shouldAuthenticate() throws Exception {
String userJSON = GSON.toJson(new ApplicationUser("eddie", "1234")); // I am using the GSON lib provided by google to
// work with json conversions
MvcResult mvcResult = mockMvc.perform(post("/login").content(userJSON)).andReturn(); // the mockMvc literally will mock a
// request to our server
String bearer = mvcResult.getResponse().getContentAsString(); // if everything goes okay, we will receive an unempty string
assertThat(!bearer.isEmpty()); // just validate :D
}
}

THANK YOU!!! ENJOY THE ARTICLE <3, LEAVE YOUR LIKE HERE SWEET!!

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay