7 Steps to Secure Spring Boot 2 REST API with Spring Security Basic Authentication, Role based Authorization and MySQL Database

In the previous article we have integrated Swagger 2 with Spring Boot REST CRUD API. Now we are gonna secure the REST API with Spring Security Basic Authentication and Role Based Authorization.

Authentication vs Authorization

Authentication is the process of verifying who you are, while authorization is the process of verifying what you have access to.

Let’s Get Started

Step 1: Add Spring Security dependencies

pom.xml

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

Step 2: Create JPA Domain Entities

Role.java

@Getter and @Setter annotations are used to generate getter and setter methods respectively. @Data annotation should not be used here since we have implemented hashCode and equals methods.

We have used Set to define many-to-many association to User. If we use a Set, the entities have to have equals() and hashCode() methods.

@ManyToMany annotation is used for specifying many-to-many relationships in Hibernate. Here, any user can have many roles and any role can have many users which leads to many-to-many associtation between the two.

package com.javachinna.model;

import java.io.Serializable;
import java.util.Objects;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * The persistent class for the role database table.
 * 
 */
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Role implements Serializable {
	private static final long serialVersionUID = 1L;
	public static final String USER = "USER";
	public static final String ADMIN = "ADMIN";
	public static final String ROLE_USER = "ROLE_USER";
	public static final String ROLE_ADMIN = "ROLE_ADMIN";

	@Id
	@Column(name = "ROLE_ID")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int roleId;

	private String name;

	// bi-directional many-to-many association to User
	@ManyToMany(mappedBy = "roles")
	private Set<UserEntity> users;

	public Role(String name) {
		this.name = name;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		return Objects.equals(name, ((Role) obj).getName());
	}

	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("Role [name=").append(name).append("]").append("[id=").append(roleId).append("]");
		return builder.toString();
	}
}

UserEntity.java

package com.javachinna.model;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
public class UserEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID")
	private Long id;
	private String username;
	private String password;
	// bi-directional many-to-many association to Role
	@ManyToMany
	@JoinTable(name = "user_role", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ROLE_ID") })
	private Set<Role> roles;

	public UserEntity(String username, String password) {
		this.username = username;
		this.password = password;
	}
}

Step 3: Create JPA Repositories

RoleRepository.java

package com.javachinna.repo;

import org.springframework.data.jpa.repository.JpaRepository;

import com.javachinna.model.Role;

public interface RoleRepository extends JpaRepository<Role, Long> {
	Role findByName(String name);
}

UserRepository.java

package com.javachinna.repo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.javachinna.model.UserEntity;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {

	UserEntity findByUsername(String userName);
}

Step 4: Implement Spring Security UserDetailsService

UserDetailsServiceImpl implements the Spring Security UserDetailsService interface. It overrides the loadUserByUsername for fetching user details by username from the database. This method will be called to authenticate and load user detials including information about the user’s granted authorities. Here we are getting the user details from the MySQL database using the Spring JPA repository. Also the password for a user is stored in encrypted format using BCrypt. You can generate the Bcrypt for a password using the Online Bcrypt Generator.

UserDetailsServiceImpl.java

package com.javachinna.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 org.springframework.transaction.annotation.Transactional;

import com.javachinna.model.Role;
import com.javachinna.model.UserEntity;
import com.javachinna.repo.UserRepository;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private UserRepository repo;

	@Override
	@Transactional
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		UserEntity user = repo.findByUsername(username);
		if (user != null) {
			return new User(user.getUsername(), user.getPassword(), buildSimpleGrantedAuthorities(user.getRoles()));
		} else {
			throw new UsernameNotFoundException("User not found with username: " + username);
		}
	}

	private static List<SimpleGrantedAuthority> buildSimpleGrantedAuthorities(final Set<Role> roles) {
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		for (Role role : roles) {
			authorities.add(new SimpleGrantedAuthority(role.getName()));
		}
		return authorities;
	}
}

Step 5: Configure Basic Authentication

Profiles.java

This class is used to define various Spring profiles. For now, let us create a constant for basicauth profile

package com.javachinna.config;

public class Profiles {

	private Profiles() {
	}

	public static final String BASIC_AUTH = "basicauth";

}

BasicAuthSecurityConfig.java

@Profile annotation indicates that a component is eligible for registration when one or more specified profiles are active. Here, Basic authentication will be enabled only when the application is run with “BasicAuth” profile.

package com.javachinna.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.javachinna.model.Role;

@Profile(Profiles.BASIC_AUTH)
@Configuration
@EnableWebSecurity
public class BasicAuthSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// Disable CSRF
		http.csrf().disable()
				// Only admin can perform HTTP delete operation
				.authorizeRequests().antMatchers(HttpMethod.DELETE).hasRole(Role.ADMIN)
				// any authenticated user can perform all other operations
				.antMatchers("/products/**").hasAnyRole(Role.ADMIN, Role.USER).and().httpBasic()
				// Permit all other request without authentication
				.and().authorizeRequests().anyRequest().permitAll()
				// We don't need sessions to be created.
				.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
	}

	@Override
	public UserDetailsService userDetailsService() {
		return userDetailsService;
	}

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

Step 6: Configure Swagger with Basic Authentication

SwaggerConfig.java

In order to enable Basic Authentication in Swagger-UI, we need to configure the Security Schemes and Security Contexts for Swagger as highlighted below

package com.javachinna.config;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.BasicAuth;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
	private static final String BASIC_AUTH = "basicAuth";

	@Bean
	public Docket api() {
		return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.basePackage("com.javachinna")).paths(PathSelectors.any()).build().apiInfo(apiInfo())
				.securitySchemes(securitySchemes()).securityContexts(List.of(securityContext()));
	}

	private ApiInfo apiInfo() {
		return new ApiInfo("Product REST API", "Product API to perform CRUD opertations", "1.0", "Terms of service",
				new Contact("Java Chinna", "www.javachinna.com", "java4chinna@gmail.com"), "License of API", "API license URL", Collections.emptyList());
	}

	private List<SecurityScheme> securitySchemes() {
		return List.of(new BasicAuth(BASIC_AUTH));
	}

	private SecurityContext securityContext() {
		return SecurityContext.builder().securityReferences(Arrays.asList(basicAuthReference())).forPaths(PathSelectors.any()).build();
	}

	private SecurityReference basicAuthReference() {
		return new SecurityReference(BASIC_AUTH, new AuthorizationScope[0]);
	}
}

Step 7: Initialize Database

SetupDataLoader.java

This class is responsible for inserting the users, roles into the db if it doesn’t exist on application startup.

package com.javachinna.config;

import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.model.Role;
import com.javachinna.model.UserEntity;
import com.javachinna.repo.RoleRepository;
import com.javachinna.repo.UserRepository;

@Component
public class SetupDataLoader implements ApplicationListener<ContextRefreshedEvent> {

	private boolean alreadySetup = false;

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private RoleRepository roleRepository;

	@Override
	@Transactional
	public void onApplicationEvent(final ContextRefreshedEvent event) {
		if (alreadySetup) {
			return;
		}

		// Create user roles
		var userRole = createRoleIfNotFound(Role.ROLE_USER);
		var adminRole = createRoleIfNotFound(Role.ROLE_ADMIN);

		// Create users
		createUserIfNotFound("user", userRole);
		createUserIfNotFound("admin", adminRole);

		alreadySetup = true;
	}

	@Transactional
	private final Role createRoleIfNotFound(final String name) {
		Role role = roleRepository.findByName(name);
		if (role == null) {
			role = new Role(name);
			role = roleRepository.save(role);
		}
		return role;
	}

	@Transactional
	private final UserEntity createUserIfNotFound(final String name, final Role role) {
		UserEntity user = userRepository.findByUsername(name);
		if (user == null) {
			user = new UserEntity(name, "$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6");
			user.setRoles(Set.of(role));
			user = userRepository.save(user);
		}
		return user;
	}
}

Run with BasicAuth Profile

You can run the application using mvn spring-boot:run -Dspring-boot.run.profiles=basicauth and hit the url http://localhost:8080/swagger-ui.html in the browser and click on the product-controller tab. All the endpoints will be secured as shown below

REST API Basic Authentication Security

Authorize API

Click on the Authorize button, enter the credentials user / password and click on Authorize button as shown below

Swagger Authorize Dialog

Test the Services

Create Product

Click on the Create Product endpoint and click on “Try it out” and then execute

Request

Create Product Request

Response

Create Product Response

Delete Product

Request

Delete Product Request

Response

Since the product can be deleted by users with “Admin” role only, we are getting HTTP Status 403 Forbidden in the response

Delete Product Response

Logout and login again with the admin credentials in the Authorize dialog and execute the delete API again. The product will be deleted.

Basic Authentication Logout

You can logout the currently authorized user in the Swagger-UI Authorize dialog as shown below and login again with a different user.

Swagger Basic Auth Logout

Method Level Role Based Authorization

Spring Security supports authorization semantics at the method level. For example, we can restrict the invocation of methods based on the user role.

Enable Method Security

MethodSecurityConfig.java

By using @EnableGlobalMethodSecurity we can easily secure our methods with Java configuration. It provides AOP security on methods, some of the annotations it will enable are PreAuthorize and PostAuthorize.

package com.javachinna.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

Secure method with Preauthorize annotation

@PreAuthorize annotation is used to specify a method access-control expression which will be evaluated to decide whether a method invocation is allowed or not

Annotate the updateProduct method in ProductController with @PreAuthorize annotation

	@Override
	@PutMapping("/products/{productId}")
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	public String updateProduct(@PathVariable(value = "productId") Long productId, @RequestBody Product product) {

In the above code, we have used the @PreAuthorize annotation with Spring-EL expression "hasRole('ROLE_ADMIN')" in order to restrict the invocation of the updateProduct method by users with admin role only.

Source Code

As always, you can get the source code from the Github below

https://github.com/JavaChinna/spring-boot-rest-basic-auth

Conclusion

That’s all folks! In this article, you’ve learned how to implement basic authentication for Spring Boot RESTful services.

I hope you enjoyed this article. Thank you for reading.

Read Next: 9 Steps to Secure Spring Boot 2 REST API with Spring Security 5 JWT Authentication, Role based Authorization and MySQL Database

Leave a Reply