Expected Behavior
A simple way to change the one-time token (OTT) generation logic via a modular way without the need of reimplementing the entire JdbcOneTimeTokenService
class for that.
Current Behavior
OTT logic is private and implemented in the in-line fashion.
Context
First of all, thanks for the OTT implementation within the Spring Security. That's great to have a smooth opportunity to use a modern passwordless sign-in approach.
Unlike the rest of Spring Security, the OTT implementation is less flexible in terms of overriding some part or changing defined assumptions. For example, among other things, JdbcOneTimeTokenService
hardcodes UUID token generation with no extension point. This prevents customization for real-world use cases without copying the entire ~300 line class.
Use Cases Requiring Custom Token Generation
Dashless UUIDs for better email UX For example, UUIDs with dashes (a830c444-29d8-4d98-9b46-6aba7b22fe5b) cannot be double-clicked to select in email clients, requiring users to manually select or triple-click. Removing dashes (a830c44429d84d989b466aba7b22fe5b) solves this while maintaining security.
Format-specific tokens for different channels
- SMS: shorter codes with appropriate rate limiting
- Email: URL-safe variants
- QR codes: optimized encoding
Custom entropy sources
Organizations with specific FIPS/compliance requirements for random number generation
Token prefixing/namespacing
Multi-tenant applications requiring token identification (e.g., tenant1_a830c444-...)
Token with a signature to identify the issuer
Here is current implementation:
public final class JdbcOneTimeTokenService implements OneTimeTokenService {
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
// hardcoded UUID generation with no extension point
String token = UUID.randomUUID().toString();
// ...
}
}
(btw, I'm using JdbcOneTimeTokenService
here as an example, but the same applies to InMemoryOneTimeTokenService
too)
The final
class modifier and hardcoded generation logic prevent customization without:
- copying the entire implementation (~300 lines) to replace 1 line of code
- losing Spring Security updates
- duplicating database schema handling, cleanup scheduling, etc.
Proposed Solutions in the order of preference
Option 1: Separate token generator interface
Create a dedicated interface for token generation:
@FunctionalInterface
public interface OneTimeTokenGenerator {
/**
* Generate a token value for the given request.
* @param request the generation request
* @return the generated token value
*/
String generate(GenerateOneTimeTokenRequest request);
}
Provide a default UUID implementation:
public final class UuidOneTimeTokenGenerator implements OneTimeTokenGenerator {
@Override
public String generate(GenerateOneTimeTokenRequest request) {
return UUID.randomUUID().toString();
}
}
Make a use of the interface within the JdbcOneTimeTokenService
:
public final class JdbcOneTimeTokenService implements OneTimeTokenService {
private OneTimeTokenGenerator tokenGenerator = new UuidOneTimeTokenGenerator();
// using setter here as an example, as we can extend the constructor as well with this param
public void setTokenGenerator(OneTimeTokenGenerator tokenGenerator) {
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.tokenGenerator = tokenGenerator;
}
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
String token = this.tokenGenerator.generate(request);
// ... rest
}
}
Changing the generation logic becomes simple and in the spirit of Spring:
@Bean
public OneTimeTokenGenerator dashlessTokenGenerator() {
return request -> UUID.randomUUID().toString().replace("-", "");
}
@Bean
public OneTimeTokenService oneTimeTokenService(
JdbcTemplate jdbcTemplate,
OneTimeTokenGenerator tokenGenerator) {
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcTemplate);
// setter or via a new constructor that accepts `OneTimeTokenGenerator` alongside `jdbcTemplate`
service.setTokenGenerator(tokenGenerator);
return service;
}
Option 2: Configurable token generator function
More functional-programming way, not sure whether it's in the Spring spirit, though
Add a setter for a token generation function:
public final class JdbcOneTimeTokenService implements OneTimeTokenService {
private Function<GenerateOneTimeTokenRequest, String> tokenGenerator =
request -> UUID.randomUUID().toString();
public void setTokenGenerator(Function<GenerateOneTimeTokenRequest, String> tokenGenerator) {
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.tokenGenerator = tokenGenerator;
}
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
String token = this.tokenGenerator.apply(request);
// ... rest of implementation
}
}
The usage is:
@Bean
public OneTimeTokenService oneTimeTokenService(JdbcTemplate jdbcTemplate) {
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcTemplate);
service.setTokenGenerator(request ->
UUID.randomUUID().toString().replace("-", "")
);
return service;
}
Option 3: Add default method to OneTimeTokenService interface
Add token generation as a default method:
public interface OneTimeTokenService {
// existing code
default String generateTokenValue(GenerateOneTimeTokenRequest request) {
return UUID.randomUUID().toString();
}
}
This will change the code of JdbcOneTimeTokenService
to:
public final class JdbcOneTimeTokenService implements OneTimeTokenService {
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
// ACTUAL CHANGE BEGIN
String token = generateTokenValue(request);
// ACTUAL CHANGE END
Instant expiresAt = this.clock.instant().plus(request.getExpiresIn());
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
insertOneTimeToken(oneTimeToken);
return oneTimeToken;
}
}
The usage is:
@Bean
public OneTimeTokenService oneTimeTokenService(JdbcTemplate jdbcTemplate) {
return new JdbcOneTimeTokenService(jdbcTemplate) {
@Override
public String generateTokenValue(GenerateOneTimeTokenRequest request) {
return UUID.randomUUID().toString().replace("-", "");
}
};
}
Option 4: Extract generation to protected method
A warning: This requires removing the final modifier from the class, which may not align with Spring Security's design philosophy.
public class JdbcOneTimeTokenService implements OneTimeTokenService {
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
String token = generateTokenValue(request);
// ... rest of implementation
}
protected String generateTokenValue(GenerateOneTimeTokenRequest request) {
return UUID.randomUUID().toString();
}
}
Options 1 and 2 are the least intrusive, follow the Spring spirit, and are safe to do.
Option 3 is more intrusive, but it encapsulates the default logic into one place.
Option 4 is the most intrusive and has the con of removing the final
from the class level, which might not be the best way to proceed.
Option 1 looks like the best one for me.
Related Improvements (for future consideration)
While keeping this issue focused on token generation, there are related customization points that would benefit from similar treatment, and this ticket can be a starting point for them.
Table name customization
Currently hardcoded as "one_time_tokens", problematic for:
- Non-public PostgreSQL schemas (e.g., myapp.one_time_tokens)
- Multi-tenant applications
- Organizational table naming conventions (e.g.,
auth_
prefix for auth-related tables) - Forced UPPERCASE-only names can become a problem in the case-sensitive DBs
Column name customization
For integrating with existing schemas
SQL query customization
Similar to JdbcDaoImpl.DEF_USERS_BY_USERNAME_QUERY
The approach of keeping those hardcoded without a simple way to override them is inconsistent with the rest of the Spring ecosystem. For example, Spring Sessions (used alongside the Spring Security) provide a way to override the table name via spring.session.jdbc.table-name
property, annotation params, etc.
It would be great to achieve the similar level of flexibility from the OTT part.
These could be addressed in separate issues if the approach for token generation is successful.
Additional Context
The OTT feature is excellent and fills a real need for passwordless authentication. These extension points would make it production-ready for diverse deployment scenarios while maintaining Spring Security's security best practices.
I'm happy to submit a PR implementing the agreed-upon approach. Please let me know which option you prefer, or if you'd like a different design entirely.