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.