Spring Boot Authentication and Authorization with MySQL – Part 2

Others
9 minutes read

Introduction

In part 1 of the series, we discussed the project’s requirements, set up the project, and made the necessary configurations. In this part, we will detail the implementation process and test our application.

You can find the source code on this GitHub repository

Implementation

Dependencies & Configuration

Let’s start with the fun part. First of all, let’s add the dependencies, open pom.xml and add the following dependencies

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>

Then let’s configure Spring Datasource, JPA, and App properties. To do this, open the application.properties and add these lines

spring.datasource.url=jdbc:mysql://localhost:<PORT>/<DB_NAME>
spring.datasource.username=<USERNAME>
spring.datasource.password=<PASSWORD>

spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto= update

# App Properties
app.jwtSecret= jwtSecretKey
app.jwtExpirationMs= 86400000

Implement the models

We have 3 tables in the database: users, roles, and user_roles for many-to-many relationships.

// Role.java

@Entity
@Table(name = "roles")
public class Role {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @Enumerated(EnumType.STRING)
  @Column(length = 20)
  private ERole name;

// Constructors, setters, getters
}
// User.java

@Entity
@Table(name = "users", 
    uniqueConstraints = { 
      @UniqueConstraint(columnNames = "username"),
      @UniqueConstraint(columnNames = "email") 
    })
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @NotBlank
  @Size(max = 20)
  private String username;

  @NotBlank
  @Size(max = 50)
  @Email
  private String email;

  @NotBlank
  @Size(max = 120)
  private String password;

  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"), 
        inverseJoinColumns = @JoinColumn(name = "role_id"))
  private Set<Role> roles = new HashSet<>();

// Constructors, setters, getters
}

Implement the repositories

Now, each model above needs a repository for persisting and accessing data. In the repository package, let’s create 2 repositories.

// Role repository

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
  Optional<Role> findByName(ERole name);
}
// User repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
  Optional<User> findByUsername(String username);

  Boolean existsByUsername(String username);

  Boolean existsByEmail(String email);
}

Configure spring security

// WebSecurityConfig.java

@Configuration
@EnableGlobalMethodSecurity(
    // securedEnabled = true,
    // jsr250Enabled = true,
    prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  UserDetailsServiceImpl userDetailsService;

  @Autowired
  private AuthEntryPointJwt unauthorizedHandler;

  @Bean
  public AuthTokenFilter authenticationJwtTokenFilter() {
    return new AuthTokenFilter();
  }

  @Override
  public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

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

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeRequests().antMatchers("/api/auth/**").permitAll()
            .antMatchers("/api/test/**").permitAll()
            .anyRequest().authenticated();

    http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
  }
}

Let me explain the code above.

@EnableGlobalMethodSecurity provides AOP security on methods. It enables @PreAuthorize, @PostAuthorize, it also supports JSR-250. You can find more parameters in the configuration in Method Security Expressions.

We override the configure(HttpSecurity http) method from the 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 the 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 the user’s information such as username, password, and authorities from an Authentication object.

Authentication authentication = 
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(username, password)
        );

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

If we want to get more data (id, email…), we can create an implementation of this UserDetails interface.

// UserDetailsImpl.java

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);
  }
}

Notice from the code above that we convert Set<Role> into List<GrantedAuthority>. It is important to work with spring security and authentication objects later.

Then, we need UserDetailsService for getting UserDetails object. You can look at UserDetailsService interface which has only one method:

public interface UserDetailsService {
     UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

So we implement it and override loadUserByUsername() method.

// UserDetailsServiceImpl.java

@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 a full custom user object using the UserRepository, then we build a UserDetails object using the static build() method.

Filter the requests

Let’s define a filter that executes once per request. Create AuthTokenFilter class that extends OncePerRequestFilter and overrides the doFilterInternal() method.

// AuthTokenFilter.java

public class AuthTokenFilter extends OncePerRequestFilter {
	@Autowired
	private JwtUtils jwtUtils;

	@Autowired
	private UserDetailsServiceImpl userDetailsService;

	private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {
			String jwt = parseJwt(request);
			if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
				String username = jwtUtils.getUserNameFromJwtToken(jwt);

				UserDetails userDetails = userDetailsService.loadUserByUsername(username);
				UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
						userDetails, null, userDetails.getAuthorities());
				authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

				SecurityContextHolder.getContext().setAuthentication(authentication);
			}
		} catch (Exception e) {
			logger.error("Cannot set user authentication: {}", e);
		}

		filterChain.doFilter(request, response);
	}

	private String parseJwt(HttpServletRequest request) {
		String headerAuth = request.getHeader("Authorization");

		if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
			return headerAuth.substring(7, headerAuth.length());
		}

		return null;
	}
}

What we do inside doFilterInternal():

  • Get JWT from the authorization header (by removing Bearer prefix)
  • If the request has JWT, validate it, and parse username from it
  • From the username, get UserDetails to create an authentication object
  • Set the current UserDetails in SecurityContext using setAuthentication(authentication) method.

After this, every time we want to get the UserDetails, we will just use the SecurityContext like this:

UserDetails userDetails =
	(UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

Implement the JWT utility class

This class has 3 functions:

  • Generate a JWT from the username, date, expiration, and secret
  • Get the username from JWT
  • Validate a JWT
// JwtUtils.java

@Component
public class JwtUtils {
	private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

	@Value("${app.jwtSecret}")
	private String jwtSecret;

	@Value("${app.jwtExpirationMs}")
	private int jwtExpirationMs;

	public String generateJwtToken(Authentication authentication) {

		UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

		return Jwts.builder()
				.setSubject((userPrincipal.getUsername()))
				.setIssuedAt(new Date())
				.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
				.signWith(SignatureAlgorithm.HS512, jwtSecret)
				.compact();
	}

	public String getUserNameFromJwtToken(String token) {
		return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
	}

	public boolean validateJwtToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException e) {
			logger.error("Invalid JWT signature: {}", e.getMessage());
		} catch (MalformedJwtException e) {
			logger.error("Invalid JWT token: {}", e.getMessage());
		} catch (ExpiredJwtException e) {
			logger.error("JWT token is expired: {}", e.getMessage());
		} catch (UnsupportedJwtException e) {
			logger.error("JWT token is unsupported: {}", e.getMessage());
		} catch (IllegalArgumentException e) {
			logger.error("JWT claims string is empty: {}", e.getMessage());
		}

		return false;
	}
}

Remember that we’ve added app.jwtSecret and app.jwtExpirationMs properties in the application.properties file.

Handle Authentication Exception

Now, we will create AuthEntryPointJwt class that implements the AuthenticationEntryPoint interface. Then we will override the commence() method. This method will be triggered anytime an unauthenticated user requests a secured HTTP resource and an AuthenticationException is thrown.

// AuthEntryPointJwt.java

@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {

	private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		logger.error("Unauthorized error: {}", authException.getMessage());
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
	}

}

HttpServletResponse.SC_UNAUTHORIZED is the 401 status code. It indicates that the request requires HTTP authentication.

At this point, we implemented all things for spring security. The next sections of this tutorial will show you how to implement controllers for our RestAPIs.

Define payloads for the spring controller

Let me summarize the payloads for our RestAPIs:

  • Requests:
    • LoginRequest: { username, password }
    • SignupRequest: { username, email, password }
  • Responses:
    • JwtResponse: { token, type, id, username, email, roles }
    • MessageResponse: { message }

To shorten this tutorial. I didn’t show these POJOs (plain old JavaScript objects) here. You can find details for payload classes in the source code of the project on GitHub.

Create the RestAPIs controllers

The authentication 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 the user to the database using UserRepository
  • /api/auth/signin
    • Authenticate { username, pasword }
    • Update SecurityContext using Authentication object
    • Generate JWT
    • Get the UserDetails from the authentication object
    • The response contains JWT and UserDetails data
// AuthController.java

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
  ...

  @PostMapping("/signin")
  public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
    ...
  }

  @PostMapping("/signup")
  public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
    ...
  }
}

The testing controller has 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
// TestController.java

@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.";
  }
}

Testing

Testing setup

Run Spring Boot application with the command `mvn spring-boot:run`.

The tables that we define in the models’ package will be automatically generated in the database. If you check the MySQL database, you can see things like this:

mysql> describe users;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| email    | varchar(50)  | YES  | UNI | NULL    |                |
| password | varchar(120) | YES  |     | NULL    |                |
| username | varchar(20)  | YES  | UNI | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> describe roles;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(20) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> describe user_roles;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| user_id | bigint(20) | NO   | PRI | NULL    |       |
| role_id | int(11)    | NO   | PRI | NULL    |       |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

We also need to add some rows into the roles table before assigning any role to the user. Run the following SQL insert statements:

INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');

API testing

Now let’s test our APIs. We will register some users with /signup API:

UsernameRole
adminROLE_ADMIN
adminModROLE_MODERATOR and ROLE_USER
userROLE_USER

Then, the users, roles, and user_role table will look like this

Now, let’s access the public resources with GET /api/test/all API

And let’s try to access a protected resource using the GET /api/test/user API  without authentication

Let’s login using the POST /api/auth/signin API

Now, copy the accessToken, and let’s access a protected resource.

Conclusion

In this blog series, we have explored how to create a secure authentication and authorization system using Spring Boot. Starting from the basics of Spring Boot, we progressed to more advanced concepts and eventually added the security layer to our project.

Throughout the series, we discussed various components of the project, including the data model, database connection, and user authentication. We also explored how to integrate JSON Web Tokens (JWT) for secure authentication and authorization.

By following this series, you should have a solid understanding of how to build a secure authentication and authorization system with Spring Boot. Whether you are a beginner or an advanced developer, this series will provide you with valuable insights and techniques for building secure web applications.

Leave a Reply

Your email address will not be published. Required fields are marked *