Securing Microservices with Spring Security: Implementing JWT and OAuth 2.0 Part-1

ayshriv

Ayush Shrivastava

Posted on July 31, 2024

Securing Microservices with Spring Security: Implementing JWT and OAuth 2.0 Part-1

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

https://start.spring.io/

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>
Enter fullscreen mode Exit fullscreen mode

Project Setup

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.

Project Structure

Files Structure

File 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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
  • 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);
}

Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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"));
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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));
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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);

    }

}
Enter fullscreen mode Exit fullscreen mode
  • 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.

View the Project on GitHub

💖 💪 🙅 🚩
ayshriv
Ayush Shrivastava

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