Securing Microservices with Spring Security: Implementing JWT and OAuth 2.0 Part-1
Ayush Shrivastava
Posted on July 31, 2024
Securing microservices using Spring Security, JWT, and OAuth 2.0 involves setting up a comprehensive authentication and authorization framework. Spring Security provides the core framework, which is configured to use JWT (JSON Web Tokens) for securely transmitting user information. OAuth 2.0 defines the protocol for obtaining and managing tokens, ensuring that authentication and authorization processes are standardized and secure. In this setup, data models such as User and Role entities are defined to manage users and their permissions, with associations reflecting the relationships between them. Spring Data JPA is utilized to interact with a MySQL database, enabling efficient data management.
REST controllers are created to handle various operations, including user authentication and management. To enhance security, sensitive information like JWT tokens is stored in Http Only cookies, which are inaccessible to client-side scripts. Authentication filters are employed to intercept requests, performing necessary checks to validate tokens and user permissions. Exception handling mechanisms are implemented to manage errors gracefully, providing clear and consistent responses. This approach not only secures the microservices but also ensures a smooth and reliable user experience, leveraging the strengths of Spring Security, OAuth 2.0, and JWT in a cohesive and effective manner.
Project Setup
Spring Initializer
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Project Structure
Creating a well-organized project structure is crucial for maintaining and scaling Java microservices. Below is a typical project structure for a Java Spring Boot microservices application, which includes packages for different layers and components.
Files Structure
Database Setup
application.properties
spring.application.name=auth-service
spring.datasource.url=jdbc:mysql://localhost:3306/auth_service
spring.datasource.username=root
spring.datasource.password=ayush@123
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
logging.level.org.springframework.security=TRACE
Store User using JPA
- Create a UserInfoEntity to store User details.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="USER_INFO")
public class UserInfoEntity {
@Id
@GeneratedValue
private Long id;
@Column(name = "USER_NAME")
private String userName;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Column(name = "ADDRESS")
private String address;
@Column(nullable = false, name = "EMAIL_ID", unique = true)
private String emailId;
@Column(nullable = false, name = "PASSWORD")
private String password;
@Column(name = "MOBILE_NUMBER")
private String mobileNumber;
@Column(nullable = false, name = "ROLES")
private String roles;
@Column(nullable = false, name = "IS_ACTIVE")
private boolean isActive;
}
- Create a file UserInfoRepository in repository package, to create jpa-mapping using hibernate.
public interface UserInfoRepository extends JpaRepository<UserInfoEntity, Long> {
Optional<UserInfoEntity> findByEmailId(String emailId);
}
- Create a UserInfoConfig class which implements UserDetails interface, which provides core user information which is later encapsulated into Authentication objects.
package com.narainox.auth_service.config;
import com.narainox.auth_service.entity.UserInfoEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Arrays;
import java.util.Collection;
@RequiredArgsConstructor
public class UserInfoConfig implements UserDetails {
private final UserInfoEntity userInfoEntity;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays
.stream(userInfoEntity
.getRoles()
.split(","))
.map(SimpleGrantedAuthority::new)
.toList();
}
@Override
public String getPassword() {
return userInfoEntity.getPassword();
}
@Override
public String getUsername() {
return userInfoEntity.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- Create a UserInfoManagerConfig class that implements the UserDetailsService interface, used to retrieve user-related data, using loadUserByUsername(), and returns UserDetails.
@RequiredArgsConstructor
@Service
public class UserInfoConfigManager implements UserDetailsService {
//Get me the user in form of auth object
private final UserInfoRepository userInfoRepository;
@Override
public UserDetails loadUserByUsername(String emailId) throws UsernameNotFoundException {
return userInfoRepository.findByEmailId(emailId)
.map(UserInfoConfig::new)
.orElseThrow(() -> new UsernameNotFoundException("UserEmail:" + emailId + " not found"));
}
}
- Let's modify our Security Setting, to let it access the API using our User. Create a SecurityConfig file in config package.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserInfoConfigManager userInfoManagerConfig;
@Order(1)
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity httpSecurity) throws Exception{
return httpSecurity
.securityMatcher(new AntPathRequestMatcher("/api/**"))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.userDetailsService(userInfoManagerConfig)
.httpBasic(withDefaults())
.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- Let's create a package called userConfig and add few users to the database using CommandlineRunner here we are only setting the essential details.
@RequiredArgsConstructor
@Component
@Slf4j
public class InitialUserInfo implements CommandLineRunner {
private final PasswordEncoder passwordEncoder;
private final UserInfoRepository userInfoRepo;
@Override
public void run(String... args) throws Exception {
UserInfoEntity manager = new UserInfoEntity();
manager.setUserName("Manager");
manager.setPassword(passwordEncoder.encode("password"));
manager.setRoles("ROLE_MANAGER");
manager.setEmailId("manager@manager.com");
UserInfoEntity admin = new UserInfoEntity();
admin.setUserName("Admin");
admin.setPassword(passwordEncoder.encode("password"));
admin.setRoles("ROLE_ADMIN");
admin.setEmailId("admin@admin.com");
UserInfoEntity user = new UserInfoEntity();
user.setUserName("User");
user.setPassword(passwordEncoder.encode("password"));
user.setRoles("ROLE_USER");
user.setEmailId("user@user.com");
userInfoRepo.saveAll(List.of(manager,admin,user));
}
}
- Add the Endpoints to access in controller package: DashboardController.java
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class DashboardController {
@PreAuthorize("hasAnyRole('ROLE_MANAGER','ROLE_ADMIN','ROLE_USER')")
@GetMapping("/welcome-message")
public ResponseEntity<String> getFirstWelcomeMessage(Authentication authentication){
return ResponseEntity.ok("Welcome to the JWT Tutorial:"+authentication.getName()+"with scope:"+authentication.getAuthorities());
}
@PreAuthorize("hasRole('ROLE_MANAGER')")
@GetMapping("/manager-message")
public ResponseEntity<String> getManagerData(Principal principal){
return ResponseEntity.ok("Manager::"+principal.getName());
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin-message")
public ResponseEntity<String> getAdminData(@RequestParam("message") String message, Principal principal){
return ResponseEntity.ok("Admin::"+principal.getName()+" has this message:"+message);
}
}
- Test the API in PostMan
http://localhost:8080/api/welcome-message : Accessed by all
http://localhost:8080/api/manager-message : Manager and Admin
http://localhost:8080/api/admin-message : Only Admin Params
The basic authentication setup for securing microservices using Spring Security, JWT, and OAuth 2.0 is complete. This setup provides a robust framework for managing user authentication and authorization.
You can find the complete source code for this project on GitHub. Feel free to explore the repository, clone it, and experiment with the code.
Posted on July 31, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
July 31, 2024