DEV Community

Yen Colon
Yen Colon

Posted on

Understanding Spring Security: Building an Authentication Flow in Less Than a Day

Recently, while experimenting with Spring Boot, I decided to implement an authentication flow. I started by reading documentation and watching tutorials. While these resources were helpful, I often encountered "blind spots" when it came to understanding how Spring Security works under the hood.

In this brief article, I’ll explain how Spring Security works and guide you through the steps to set up an authentication flow in less than a day.


Step 1: Choose the Right Libraries

First, you need to add the necessary dependencies. In our case, we’ll use Spring Boot Security Starter and Java JWT.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

For handling JSON Web Tokens (JWTs), we’ll later add dependencies for JJWT.


Step 2: Configure Spring Security

With the dependencies installed, we can configure Spring Security. Here’s a diagram of the key components involved:

Spring security

To get started, create a new class annotated with @Configuration. In this class, we’ll define a few essential @Bean methods, including:

  • PasswordEncoder: For securely storing passwords.
  • SecurityFilterChain: To define the security filter logic.
  • AuthenticationManager: To handle authentication processes.

PasswordEncoder

This @Bean ensures that passwords are securely hashed before being stored in the database.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
Enter fullscreen mode Exit fullscreen mode

SecurityFilterChain

This @Bean allows you to define security rules for incoming HTTP requests, such as authorization checks and filter chains.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http.csrf().disable()
            .authorizeRequests(auth -> auth
                    .antMatchers("/login", "/register").permitAll()
                    .anyRequest().authenticated())
            .httpBasic().and()
            .build();
}
Enter fullscreen mode Exit fullscreen mode

AuthenticationManager

The AuthenticationManager coordinates authentication by delegating the process to configured AuthenticationProvider instances.

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement UserDetailsService

Now that the security configuration is set up, the next step is to implement the UserDetailsService. But why is this necessary?

The UserDetailsService is responsible for retrieving user details from the database, allowing us to verify credentials during authentication.

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                new ArrayList<>());
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Add JWT Utilities

With the basics in place, it’s time to configure JWT handling. For this, we’ll use the JJWT library. Add the following dependencies:

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-api</artifactId>
   <version>0.12.6</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.12.6</version>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.12.6</version>
  <scope>runtime</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Example: JWT Utility Class

@Component
public class JwtUtils {

    private final String jwtSecret = "yourSecretKey";
    private final int jwtExpirationMs = 86400000;

    public String generateJwtToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public boolean validateJwtToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Secure sensitive keys: Use environment variables or a secrets manager for your jwtSecret.
  • Hash passwords securely: Always use a strong hashing algorithm like BCrypt.
  • Test your configuration: Use tools like Postman or cURL to ensure everything works as expected.
  • Implement role-based access control: Customize access based on user roles.

Common Pitfalls

  • Disabling CSRF inappropriately: Make sure to disable CSRF only for stateless APIs.
  • Using plaintext passwords: Never store plaintext passwords; always use a PasswordEncoder.
  • Hardcoding sensitive values: Avoid hardcoding secrets like the JWT key.

Conclusion and Next Steps

By following these steps, you can set up a complete authentication flow in a day. Spring Security’s powerful features might seem overwhelming at first, but with a structured approach, you can harness its capabilities effectively.

For further learning, consider:

  • Adding refresh tokens to your flow.
  • Exploring role-based access control with Spring Security.
  • Integrating OAuth2 for more advanced authentication flows.

Top comments (0)