Implementing One-Time Token Authentication with Spring Security
Raviteja Daggupati
Posted on November 18, 2024
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
Posted on November 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.