In today's digital landscape, providing secure and user-friendly authentication methods is crucial. One such method gaining popularity is One-Time Token (OTT) authentication, often implemented as "magic links" sent via email. Spring Security 6.4.0 provides robust built-in support for OTT authentication, including ready-to-use implementations. In this comprehensive guide, we'll explore how to implement secure OTT authentication using both built-in solutions and custom implementations.
Understanding One-Time Tokens vs. One-Time Passwords
Before diving into implementation, it's important to understand that One-Time Tokens (OTT) differ from One-Time Passwords (OTP). While OTP systems typically require initial setup and rely on external tools for password generation, OTT systems are simpler from a user perspective - they receive a unique token (usually via email) that they can use to authenticate.
Key differences include:
- OTT doesn't require initial user setup
- Tokens are generated and delivered by your application
- Each token is typically valid for a single use and expires after a set time
Available Built-in Implementations
Spring Security provides two implementations of OneTimeTokenService
:
-
InMemoryOneTimeTokenService:
- Stores tokens in memory
- Ideal for development and testing
- Not suitable for production or clustered environments
- Tokens are lost on application restart
-
JdbcOneTimeTokenService:
- Stores tokens in a database
- Suitable for production use
- Works in clustered environments
- Persistent storage with automatic cleanup
Using InMemoryOneTimeTokenService
Here's how to implement the simpler in-memory solution:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/ott/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); // Uses InMemoryOneTimeTokenService by default return http.build(); } }
Using JdbcOneTimeTokenService
For production environments, use the JDBC implementation:
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired JdbcTemplate jdbcTemplate; @Bean public OneTimeTokenService oneTimeTokenService() { return new JdbcOneTimeTokenService(jdbcTemplate); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/ott/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); return http.build(); } }
Required table structure for JdbcOneTimeTokenService:
CREATE TABLE one_time_tokens ( token_value VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL, issued_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL, used BOOLEAN NOT NULL );
Custom Implementation
For more control over the token generation and validation process, you can create a custom implementation:
1. Token Entity and Repository
@Entity @Table(name = "one_time_tokens") public class OneTimeToken { @Id @GeneratedValue private Long id; private String tokenValue; private String username; private LocalDateTime createdAt; private LocalDateTime expiresAt; private boolean used; // Getters and setters omitted for brevity } @Repository public interface OneTimeTokenRepository extends JpaRepository<OneTimeToken, Long> { Optional<OneTimeToken> findByTokenValueAndUsedFalse(String tokenValue); void deleteByExpiresAtBefore(LocalDateTime dateTime); }
2. Custom Token Service
@Service @Transactional public class PersistentOneTimeTokenService implements OneTimeTokenService { private static final int TOKEN_VALIDITY_MINUTES = 15; @Autowired private OneTimeTokenRepository tokenRepository; @Override public OneTimeToken generate(GenerateOneTimeTokenRequest request) { String tokenValue = UUID.randomUUID().toString(); LocalDateTime now = LocalDateTime.now(); OneTimeToken token = new OneTimeToken(); token.setTokenValue(tokenValue); token.setUsername(request.getUsername()); token.setCreatedAt(now); token.setExpiresAt(now.plusMinutes(TOKEN_VALIDITY_MINUTES)); token.setUsed(false); return return new DefaultOneTimeToken(token.getTokenValue(),token.getUsername(), Instant.now()); } @Override public Authentication consume(ConsumeOneTimeTokenRequest request) { OneTimeToken token = tokenRepository.findByTokenValueAndUsedFalse(request.getTokenValue()) .orElseThrow(() -> new BadCredentialsException("Invalid or expired token")); if (token.getExpiresAt().isBefore(LocalDateTime.now())) { throw new BadCredentialsException("Token has expired"); } token.setUsed(true); tokenRepository.save(token); UserDetails userDetails = loadUserByUsername(token.getUsername()); return new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); } }
Implementing Token Delivery
Spring Security doesn't handle token delivery, so you'll need to implement it:
@Component public class EmailMagicLinkHandler implements OneTimeTokenGenerationSuccessHandler { @Autowired private JavaMailSender mailSender; private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/check-email"); @Override public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken token) throws IOException, ServletException { String magicLink = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) .replacePath(request.getContextPath()) .replaceQuery(null) .fragment(null) .path("/login/ott") .queryParam("token", token.getTokenValue()) .toUriString(); SimpleMailMessage message = new SimpleMailMessage(); message.setTo(getUserEmail(token.getUsername())); message.setSubject("Your Sign-in Link"); message.setText("Click here to sign in: " + magicLink); mailSender.send(message); redirectHandler.handle(request, response, token); } }
Customizing URLs and Pages
Spring Security provides several customization options:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(ott -> ott .generateTokenUrl("/custom/generate-token") // Custom token generation URL .submitPageUrl("/custom/submit-token") // Custom token submission page .showDefaultSubmitPage(false) // Disable default submit page ); return http.build(); } }
Production Considerations
When deploying OTT authentication in production:
-
Choose the Right Implementation
- Use JdbcOneTimeTokenService or custom implementation for production
- InMemoryOneTimeTokenService should only be used for development/testing
Configure Email Delivery
spring.mail.host=smtp.your-provider.com spring.mail.port=587 spring.mail.username=your-username spring.mail.password=your-password spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true
- Security Best Practices
- Set appropriate token expiration times (15 minutes recommended)
- Implement rate limiting for token generation
- Use HTTPS for all endpoints
- Monitor failed authentication attempts
- Ensure tokens are single-use and invalidated immediately after use
- Implement automatic cleanup of expired tokens
- Use secure random token generation to prevent guessing
How It Works
- User requests a token by submitting their email address
- System generates a secure token and sends a magic link via email
- User clicks the link and is redirected to the token submission page
- System validates the token and authenticates the user if valid
Conclusion
Spring Security's OTT support provides a robust foundation for implementing secure, user-friendly authentication. Whether you choose the built-in implementations or create a custom solution, you can offer your users a passwordless login option while maintaining high security standards.
When implementing OTT authentication, remember to:
- Choose the appropriate implementation for your environment
- Implement secure token delivery
- Configure proper token expiration
- Follow security best practices
- Create user-friendly error handling and redirects
- Implement proper email templating for a professional look
By following this guide, you can implement a secure and user-friendly OTT authentication system that meets your application's needs while leveraging Spring Security's robust security features.
Reference: https://docs.spring.io/spring-security/reference/servlet/authentication/onetimetoken.html
Top comments (0)