Overview of Spring Boot JWT Authentication example
We will build a Spring Boot application in that:
User can signup new account, or login with username & password.
By User’s role (admin, moderator, user), we authorize the User to access resources
These are APIs that we need to provide:
The database we will use could be PostgreSQL or MySQL depending on the way we configure project dependency & datasource.
Spring Boot Signup & Login with JWT Authentication Flow
The diagram shows flow of how we implement User Registration, User Login and Authorization process.
A legal JWT must be added to HTTP Authorization Header if Client accesses protected resources.
Spring Boot Server Architecture with Spring Security
You can have an overview of our Spring Boot Server with the diagram below:
Now I will explain it briefly.
Spring Security
– WebSecurityConfigurerAdapter is the crux of our security implementation. It provides HttpSecurity configurations to configure cors, csrf, session management, rules for protected resources. We can also extend and customize the default configuration that contains the elements below.
– UserDetailsService interface has a method to load User by username and returns a UserDetails object that Spring Security can use for authentication and validation.
– UserDetails contains necessary information (such as: username, password, authorities) to build an Authentication object.
– UsernamePasswordAuthenticationToken gets {username, password} from login Request, AuthenticationManager will use it to authenticate a login account.
– AuthenticationManager has a DaoAuthenticationProvider (with help of UserDetailsService & PasswordEncoder) to validate UsernamePasswordAuthenticationToken object. If successful, AuthenticationManager returns a fully populated Authentication object (including granted authorities).
– OncePerRequestFilter makes a single execution for each request to our API. It provides a doFilterInternal() method that we will implement parsing & validating JWT, loading User details (using UserDetailsService), checking Authorizaion (using UsernamePasswordAuthenticationToken).
repository has intefaces that extend Spring Data JPA JpaRepository to interact with Database.
UserRepository extends JpaRepository<User, Long>
RoleRepository extends JpaRepository<Role, Long>
models defines two main models for Authentication (User) & Authorization (Role). They have many-to-many relationship.
User: id, username, email, password, roles
Role: id, name
payload defines classes for Request and Response objects
We also have application.properties for configuring Spring Datasource, Spring Data JPA and App properties (such as JWT Secret string or Token expiration time).
– @EnableWebSecurity allows Spring to find and automatically apply the class to the global Web Security.
– @EnableGlobalMethodSecurity provides AOP security on methods. It enables @PreAuthorize, @PostAuthorize, it also supports JSR-250. You can find more parameters in configuration in Method Security Expressions.
– We override the configure(HttpSecurity http) method from WebSecurityConfigurerAdapter interface. It tells Spring Security how we configure CORS and CSRF, when we want to require all users to be authenticated or not, which filter (AuthTokenFilter) and when we want it to work (filter before UsernamePasswordAuthenticationFilter), which Exception Handler is chosen (AuthEntryPointJwt).
– Spring Security will 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 configuring DaoAuthenticationProvider by AuthenticationManagerBuilder.userDetailsService() method.
– We also need a PasswordEncoder for the DaoAuthenticationProvider. If we don’t specify, it will use plain text.
Implement UserDetails & UserDetailsService
If the authentication process is successful, we can get User’s information such as username, password, authorities from an Authentication object.
If we want to get more data (id, email…), we can create an implementation of this UserDetails interface.
security/services/UserDetailsImpl.java
package com.bezkoder.springjwt.security.services;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.bezkoder.springjwt.models.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Long getId() {
return id;
}
public String getEmail() {
return email;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
Look at the code above, you can notice that we convert Set<Role> into List<GrantedAuthority>. It is important to work with Spring Security and Authentication object later.
As I have said before, we need UserDetailsService for getting UserDetails object. You can look at UserDetailsService interface that has only one method:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
So we implement it and override loadUserByUsername() method.
security/services/UserDetailsServiceImpl.java
package com.bezkoder.springjwt.security.services;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.transaction.annotation.Transactional;
import com.bezkoder.springjwt.models.User;
import com.bezkoder.springjwt.repository.UserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
In the code above, we get full custom User object using UserRepository, then we build a UserDetails object using static build() method.
Filter the Requests
Let’s define a filter that executes once per request. So we create AuthTokenFilter class that extends OncePerRequestFilter and override doFilterInternal() method.
What we do inside doFilterInternal():
– get JWT from the Authorization header (by removing Bearer prefix)
– if the request has JWT, validate it, parse username from it
– from username, get UserDetails to create an Authentication object
– set the current UserDetails in SecurityContext using setAuthentication(authentication) method.
After this, everytime you want to get UserDetails, just use SecurityContext like this:
Remember that we’ve added bezkoder.app.jwtSecret and bezkoder.app.jwtExpirationMs properties in application.properties file.
Handle Authentication Exception
Now we create AuthEntryPointJwt class that implements AuthenticationEntryPoint interface. Then we override the commence() method. This method will be triggerd anytime unauthenticated User requests a secured HTTP resource and an AuthenticationException is thrown.
To keep the tutorial not so long, I don’t show these POJOs here.
You can find details for payload classes in source code of the project on Github.
Create Spring RestAPIs Controllers
Controller for Authentication
This controller provides APIs for register and login actions.
– /api/auth/signup
check existing username/email
create new User (with ROLE_USER if not specifying role)
save User to database using UserRepository
– /api/auth/signin
authenticate { username, pasword }
update SecurityContext using Authentication object
generate JWT
get UserDetails from Authentication object
response contains JWT and UserDetails data
controllers/AuthController.java
package com.bezkoder.springjwt.controllers;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bezkoder.springjwt.models.ERole;
import com.bezkoder.springjwt.models.Role;
import com.bezkoder.springjwt.models.User;
import com.bezkoder.springjwt.payload.request.LoginRequest;
import com.bezkoder.springjwt.payload.request.SignupRequest;
import com.bezkoder.springjwt.payload.response.JwtResponse;
import com.bezkoder.springjwt.payload.response.MessageResponse;
import com.bezkoder.springjwt.repository.RoleRepository;
import com.bezkoder.springjwt.repository.UserRepository;
import com.bezkoder.springjwt.security.jwt.JwtUtils;
import com.bezkoder.springjwt.security.services.UserDetailsImpl;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok(new JwtResponse(jwt,
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity
.badRequest()
.body(new MessageResponse("Error: Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity
.badRequest()
.body(new MessageResponse("Error: Email is already in use!"));
}
// Create new user's account
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRole();
Set<Role> roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
switch (role) {
case "admin":
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
break;
case "mod":
Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(modRole);
break;
default:
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
}
Controller for testing Authorization
There are 4 APIs:
– /api/test/all for public access
– /api/test/user for users has ROLE_USER or ROLE_MODERATOR or ROLE_ADMIN
– /api/test/mod for users has ROLE_MODERATOR
– /api/test/admin for users has ROLE_ADMIN
Do you remember that we used @EnableGlobalMethodSecurity(prePostEnabled = true) for WebSecurityConfig class?
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }
Now we can secure methods in our Apis with @PreAuthorize annotation easily.
controllers/TestController.java
package com.bezkoder.springjwt.controllers;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/all")
public String allAccess() {
return "Public Content.";
}
@GetMapping("/user")
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/mod")
@PreAuthorize("hasRole('MODERATOR')")
public String moderatorAccess() {
return "Moderator Board.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
}
Run & Test
Run Spring Boot application with command: mvn spring-boot:run
Tables that we define in models package will be automatically generated in Database.
If you check PostgreSQL for example, you can see things like this:
Today we’ve learned so many interesting things about Spring Security and JWT Token based Authentication in just a Spring Boot example.
Despite we wrote a lot of code, I hope you will understand the overall architecture of the application, and apply it in your project at ease.
You can also know how to deploy Spring Boot App on AWS (for free) with this tutorial.