Spring Boot JWT Authentication with Spring Security 3.0 + MySQL + Multiple User Types REST API Tutorial

Chintan Golakiya
6 min readJun 17, 2023

--

In this post I am sharing how I implemented Spring Boot JWT token authorization and authentication with MySQL database for different user types.

Following are user details and API requirements taken

User tables in MySQL database are as follow.

admin (id, username, password)
teacher(id, email, password, name, status, createdBy (forienkey admin(id))
student(id, email, password, name, status)

API details

Admin -
register - register admin with (username, password) (public api)
login - login admin with (username, password) (public api)
Teacher -
register - add teacher with (email, name, password)
(only admin user can register teacher)
login - login teacher with (email, password) (public api)
Student -
register - register student with (email, name, password) (public api)
login - login student with (email, password) (public api)

Further APIs can be configured for authorized for particular type of user.

If you want to directly jump into code you can check following git repository — https://github.com/chintan-golakiya/exam-portal-springboot-app/tree/jwt-authentication

Following is a architecture used in spring boot jwt authentication.

Spring Boot Security Flow

Authentication Filter is similar to middleware. every http request is goes to chain of filters. we can add one filter for authentication into spring filter chain.

for Authentication, when user login we generate JWT token with username/email and user type. this authentication details are added in security context.

for Authorization, JWT token is sent in “Authorization” header of HTTP request, from Authentication Filter, authentication manager get this JWT token and validates token using UserDetailsService. if valid token is found further request reaches to RestController.

I am not adding here Entity Class, Dto classes for request and response data and JPA Repository code. (please check above shared github repo)

Create a SecurityConfig class with Configuration and EnableWebSecurity annotations. Also add SecurityFilterChain as Bean to permit all HTTP GET Method request.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET).permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();

return http.build();
}
}

Also add following AuthenticationManager and PasswordEncoder as Bean in SecurityConfig class

 @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

Following UserType enum is created in spring boot. (UserType is not entity)

public enum UserType {
ADMIN("ADMIN"), TEACHER("TEACHER"), STUDENT("STUDENT");

private final String type;

UserType(String string) {
type = string;
}

@Override
public String toString() {
return type;
}
}

JWTGenerator class for JWT Token generation, validation and extracting claim details

@Component
public class JwtGenerator {

public String generateToken(Authentication authentication, String userType) {
String username= authentication.getName();
Date currentDate = new Date();
Date expiryDate = new Date(currentDate.getTime()+ SecurityConstants.JWT_EXPIRATION);

String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(currentDate)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, SecurityConstants.JWT_SECERT)
.claim("usertype", userType)
.compact();
return token;
}

public String getUsernameFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SecurityConstants.JWT_SECERT)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}

public String getUserTypeFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SecurityConstants.JWT_SECERT)
.parseClaimsJws(token)
.getBody();
return claims.get("usertype").toString();
}

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SecurityConstants.JWT_SECERT).parseClaimsJws(token);
return true;
}
catch (Exception ex) {
throw new AuthenticationCredentialsNotFoundException("JWT token is not valid " + token);
}
}
}

RESTController function for admin register API.

@PostMapping("api/v1/adminRegister")
public ResponseEntity<String> adminRegister(@RequestBody AdminAuthDto adminAuthDto) {
if(adminRepo.existsByUsername(adminAuthDto.getUsername())) {
return new ResponseEntity<String>("Username is taken !! ",HttpStatus.BAD_REQUEST);
}
AdminEntity adminEntity = new AdminEntity();
adminEntity.setUsername(adminAuthDto.getUsername());
adminEntity.setPassword(passwordEncoder.encode(adminAuthDto.getPassword()));

adminRepo.save(adminEntity);
return new ResponseEntity<String>("User Register successfull !! ", HttpStatus.CREATED);
}

RESTController function for admin login API

@PostMapping("api/v1/adminLogin")
public ResponseEntity<AdminLoginResponseDto> login(@RequestBody AdminAuthDto adminAuthDto) {
System.out.println("adminLogin");
customUserDetailsService.setUserType(UserType.ADMIN);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(adminAuthDto.getUsername(), adminAuthDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);

String token = jwtGenerator.generateToken(authentication,UserType.ADMIN.toString());
AdminLoginResponseDto responseDto = new AdminLoginResponseDto();
responseDto.setSuccess(true);
responseDto.setMessage("login successful !!");
responseDto.setToken(token);
AdminEntity admin = adminRepo.findByUsername(adminAuthDto.getUsername()).orElseThrow();
responseDto.setAdmin(admin.getUsername(), admin.getId());
return new ResponseEntity<>(responseDto, HttpStatus.OK);
}

here, AuthenticationManager authenticates user based on admin username and password. then JWT authorization token is generated with ADMIN UserType and JWT token is sent into response.

Similar following Rest Controller function are created for Student Register/Login and Teacher Login API.

@PostMapping("api/v1/teacherLogin")
public ResponseEntity<TeacherLoginResponseDto> teacherLogin(@RequestBody TeacherLoginDto teacherLoginDto) {
System.out.println("teacherLogin");
customUserDetailsService.setUserType(UserType.TEACHER);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(teacherLoginDto.getEmail(), teacherLoginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtGenerator.generateToken(authentication, UserType.TEACHER.toString());
TeacherLoginResponseDto responseDto = new TeacherLoginResponseDto();
responseDto.setSuccess(true);
responseDto.setMessage("login successful !!");
responseDto.setToken(token);
TeacherEntity teacher = teacherRepo.findByEmail(teacherLoginDto.getEmail()).orElseThrow();
responseDto.setTeacher(teacher.getName(), teacher.getEmail(), teacher.getId());
return new ResponseEntity<TeacherLoginResponseDto>(responseDto, HttpStatus.OK);
}

@PostMapping("api/v1/studentRegister")
public ResponseEntity<SuccessandMessageDto> studentRegister(@RequestBody StudentRegisterDto studentRegisterDto) {
System.out.println("studentRegister");
SuccessandMessageDto response = new SuccessandMessageDto();
if(studentRepo.existsByEmail(studentRegisterDto.getEmail())) {
response.setMessage("Email is already registered !!");
response.setSuccess(false);
return new ResponseEntity<SuccessandMessageDto>(response, HttpStatus.BAD_REQUEST);
}
StudentEntity studentEntity = new StudentEntity();
studentEntity.setName(studentRegisterDto.getUsername());
studentEntity.setPassword(passwordEncoder.encode(studentRegisterDto.getPassword()));
studentEntity.setEmail(studentRegisterDto.getEmail());
studentEntity.setStatus(true);
studentRepo.save(studentEntity);
response.setMessage("Profile Created Successfully !!");
response.setSuccess(true);
return new ResponseEntity<SuccessandMessageDto>(response, HttpStatus.OK);
}

@PostMapping("api/v1/studentLogin")
public ResponseEntity<StudentLoginResponseDto> studentLogin(@RequestBody StudentLoginDto studentLoginDto) {
System.out.println("studentLogin");
customUserDetailsService.setUserType(UserType.STUDENT);
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(studentLoginDto.getEmail(), studentLoginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtGenerator.generateToken(authentication, UserType.STUDENT.toString());
StudentLoginResponseDto responseDto = new StudentLoginResponseDto();
responseDto.setSuccess(true);
responseDto.setMessage("login successful !!");
responseDto.setToken(token);
StudentEntity student = studentRepo.findByEmail(studentLoginDto.getEmail()).orElseThrow();
responseDto.setStudent(student.getName(), student.getEmail(), student.getId());
return new ResponseEntity<StudentLoginResponseDto>(responseDto, HttpStatus.OK);
}

CustomUserDetailsService implements UserDetailsService and based on UserType and username(username or email), UserDetails, authority is added.

@Service
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
private AdminRepo adminRepo;
@Autowired
private TeacherRepo teacherRepo;
@Autowired
private StudentRepo studentRepo;

private UserType userType;

public UserType getUserType() {
return userType;
}

public void setUserType(UserType userType) {
this.userType = userType;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println(userType);
if(userType==UserType.ADMIN) {

AdminEntity adminEntity = adminRepo.findByUsername(username).orElseThrow(()-> new UsernameNotFoundException("Admin Username "+ username+ "not found"));

SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority(UserType.ADMIN.toString());
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(adminAuthority);
return new User(adminEntity.getUsername(), adminEntity.getPassword(), authorities);
} else if(userType == UserType.TEACHER) {
TeacherEntity teacherEntity = teacherRepo.findByEmail(username).orElseThrow(()-> new UsernameNotFoundException("Teacher Email "+ username+ "not found"));

SimpleGrantedAuthority teacherAuthority = new SimpleGrantedAuthority(UserType.TEACHER.toString());
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(teacherAuthority);
return new User(teacherEntity.getEmail(), teacherEntity.getPassword(), authorities);
} else if(userType == UserType.STUDENT) {
StudentEntity studentEntity = studentRepo.findByEmail(username).orElseThrow(()-> new UsernameNotFoundException("Student Email "+ username+ "not found"));

SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority(UserType.STUDENT.toString());
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(adminAuthority);
return new User(studentEntity.getEmail(), studentEntity.getPassword(), authorities);
}
return null;
}

}

JWTAuthenticationFilter for authentication filter, which run doInternalFilter function once per request

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtGenerator jwtGenerator;
@Autowired
private CustomUserDetailsService customUserDetailsService;


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getJWTfromRequest(request);
if(token != null && jwtGenerator.validateToken(token)) {
String username = jwtGenerator.getUsernameFromJWT(token);
String userType = jwtGenerator.getUserTypeFromJWT(token);
customUserDetailsService.setUserType(UserType.valueOf(userType));
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());

authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);

}

private String getJWTfromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(bearerToken!=null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
} else {
return null;
}
}

}

JWT Authentication filter is added as Bean in SecurityConfig class

 @Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}

TeacherRegister RESTController function in AdminController class

After authentication and token validation from authentication filter in RestController function we require admin username details from jwt token to put createdBy field in teacher user details.

@RestController
@RequestMapping("/api/v1/admin")
public class AdminController {

@Autowired
private JwtGenerator jwtGenerator;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TeacherRepo teacherRepo;
@Autowired
private AdminRepo adminRepo;

@PostMapping("/register")
public ResponseEntity<SuccessandMessageDto> registerTeacher(@RequestBody TeacherRegisterDto teacherRegisterDto, @RequestHeader(name="Authorization") String token) {
SuccessandMessageDto response = new SuccessandMessageDto();
if(teacherRepo.existsByEmail(teacherRegisterDto.getEmail())) {
response.setMessage("Email is already registered !!");
response.setSuccess(false);
return new ResponseEntity<SuccessandMessageDto>(response, HttpStatus.BAD_REQUEST);
}
TeacherEntity teacherEntity = new TeacherEntity();
teacherEntity.setName(teacherRegisterDto.getUsername());
teacherEntity.setPassword(passwordEncoder.encode(teacherRegisterDto.getPassword()));
teacherEntity.setEmail(teacherRegisterDto.getEmail());
teacherEntity.setStatus(true);
try {
teacherEntity.setCreatedBy(adminRepo.findByUsername(jwtGenerator.getUsernameFromJWT(token.substring(7))).orElseThrow());
} catch(Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
response.setMessage("Unauthorized request");
response.setSuccess(false);
return new ResponseEntity<SuccessandMessageDto>(response, HttpStatus.UNAUTHORIZED);
}
teacherRepo.save(teacherEntity);
response.setMessage("Profile Created Successfully !!");
response.setSuccess(true);
return new ResponseEntity<SuccessandMessageDto>(response, HttpStatus.OK);
}

Update SecurityFilterChain Bean to authenticate HTTP APIs

 @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.requestMatchers("/api/public/**").hasAuthority(UserType.TEACHER.toString())
.requestMatchers("/api/v1/adminRegister").permitAll()
.requestMatchers("/api/v1/adminLogin").permitAll()
.requestMatchers("/api/v1/teacherLogin").permitAll()
.requestMatchers("/api/v1/studentRegister").permitAll()
.requestMatchers("/api/v1/studentLogin").permitAll()
.requestMatchers("/api/v1/admin/**").hasAuthority(UserType.ADMIN.toString())
.anyRequest().authenticated()
.and()
.httpBasic();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}

all “/api/public” API is only allowed for TEACHER users

all APIs with path “/api/v1/admin/” are only allowed for ADMIN users. (“/api/v1/admin/register” API is only authorized for ADMIN user)

All other APIs enabled as Public APIs.

Further similarly we can add APIs which are only allowed for STUDENT users.

Thank you for reading complete article. Hope you enjoyed and learned something new. Happy Coding :)

--

--

Chintan Golakiya

Software Engineer | Web Developer | JavaScript Developer