docs
One-time token (OTT) authentication provides a secure, time-limited authentication mechanism commonly used for password reset flows, magic links, email verification, and other temporary access scenarios.
Core Capabilities:
OneTimeTokenService generates and consumes one-time tokensOneTimeTokenAuthenticationProviderOneTimeTokenReactiveAuthenticationManagerKey Interfaces and Classes:
OneTimeToken - Interface: getTokenValue(), getUsername(), getExpiresAt()OneTimeTokenService - Interface: generate(), consume()GenerateOneTimeTokenRequest - Request object with username and optional expirationDefaultOneTimeToken - Default implementationInMemoryOneTimeTokenService - In-memory implementation (single instance, not clustered)JdbcOneTimeTokenService - JDBC-based implementation (persistent, clustered)OneTimeTokenAuthenticationToken - Unauthenticated token with OTT valueOneTimeTokenAuthentication - Authenticated token after validationOneTimeTokenAuthenticationProvider - Authentication provider for OTT validationDefault Behaviors:
GenerateOneTimeTokenRequest)consume() returns null if token invalid, expired, or already usedgetCredentials() and getPrincipal() of OneTimeTokenAuthenticationTokenThreading Model:
Mono<Authentication> for non-blocking executionLifecycle:
generate() with username and optional expirationconsume() during authentication (removed after use)Exceptions:
InvalidOneTimeTokenException - Thrown when token invalid, expired, or already usedconsume() returns null instead of throwing (check return value)Edge Cases:
consume() (returns null if expired)GenerateOneTimeTokenRequestJdbcOneTimeTokenService (not InMemoryOneTimeTokenService)One-time tokens are:
Common use cases:
Represents a one-time use authentication token.
public interface OneTimeToken extends Serializable{ .api }
Key Methods:
String getTokenValue(){ .api }
Returns the token string value.
String getUsername(){ .api }
Returns the username associated with this token.
Instant getExpiresAt(){ .api }
Returns the expiration timestamp of the token.
Usage:
OneTimeToken token = oneTimeTokenService.generate(
new GenerateOneTimeTokenRequest("user@example.com")
);
String tokenValue = token.getTokenValue();
String username = token.getUsername();
Instant expiry = token.getExpiresAt();
// Send token via email
emailService.sendPasswordResetLink(username, tokenValue);Service interface for generating and consuming one-time tokens.
public interface OneTimeTokenService{ .api }
Key Methods:
OneTimeToken generate(GenerateOneTimeTokenRequest request){ .api }
Generates a new one-time token for the specified user.
OneTimeToken consume(String tokenValue) throws InvalidOneTimeTokenException{ .api }
Consumes (validates and removes) a one-time token. Throws InvalidOneTimeTokenException if the token is invalid, expired, or already used.
Example:
@Service
public class PasswordResetService {
private final OneTimeTokenService tokenService;
private final EmailService emailService;
public void initiatePasswordReset(String email) {
GenerateOneTimeTokenRequest request =
new GenerateOneTimeTokenRequest(email);
OneTimeToken token = tokenService.generate(request);
String resetLink = "https://example.com/reset-password?token=" +
token.getTokenValue();
emailService.sendPasswordResetEmail(email, resetLink);
}
public void validateResetToken(String tokenValue) {
try {
OneTimeToken token = tokenService.consume(tokenValue);
// Token is valid, proceed with password reset
String username = token.getUsername();
// Show password reset form for username
} catch (InvalidOneTimeTokenException e) {
// Token is invalid, expired, or already used
throw new InvalidResetTokenException("Invalid or expired reset token");
}
}
}Reactive version of the token service for non-blocking operations.
public interface ReactiveOneTimeTokenService{ .api }
Key Methods:
Mono<OneTimeToken> generate(GenerateOneTimeTokenRequest request){ .api }
Reactively generates a one-time token.
Mono<OneTimeToken> consume(String tokenValue){ .api }
Reactively consumes a one-time token.
Example:
@Service
public class ReactivePasswordResetService {
private final ReactiveOneTimeTokenService tokenService;
public Mono<Void> initiatePasswordReset(String email) {
return tokenService.generate(new GenerateOneTimeTokenRequest(email))
.flatMap(token ->
emailService.sendPasswordResetEmail(
email,
buildResetLink(token.getTokenValue())
)
)
.then();
}
public Mono<String> validateResetToken(String tokenValue) {
return tokenService.consume(tokenValue)
.map(OneTimeToken::getUsername)
.onErrorMap(
InvalidOneTimeTokenException.class,
e -> new InvalidResetTokenException("Invalid or expired token")
);
}
}Request object for generating one-time tokens.
public class GenerateOneTimeTokenRequest{ .api }
Constructor:
public GenerateOneTimeTokenRequest(String username){ .api }
Key Methods:
String getUsername(){ .api }
Returns the username for which the token should be generated.
Example:
GenerateOneTimeTokenRequest request =
new GenerateOneTimeTokenRequest("user@example.com");
OneTimeToken token = tokenService.generate(request);Default implementation of the OneTimeToken interface.
public class DefaultOneTimeToken implements OneTimeToken{ .api }
Constructor:
public DefaultOneTimeToken(String tokenValue, String username, Instant expiresAt){ .api }
Example:
DefaultOneTimeToken token = new DefaultOneTimeToken(
"abc123...",
"user@example.com",
Instant.now().plus(15, ChronoUnit.MINUTES)
);In-memory implementation suitable for single-instance applications and testing.
public class InMemoryOneTimeTokenService implements OneTimeTokenService{ .api }
Configuration:
@Configuration
public class OneTimeTokenConfig {
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new InMemoryOneTimeTokenService();
}
}Characteristics:
Usage:
@Service
public class MagicLinkService {
private final InMemoryOneTimeTokenService tokenService;
public String generateMagicLink(String email) {
OneTimeToken token = tokenService.generate(
new GenerateOneTimeTokenRequest(email)
);
return "https://example.com/login?token=" + token.getTokenValue();
}
public boolean validateMagicLink(String tokenValue) {
try {
OneTimeToken token = tokenService.consume(tokenValue);
return true;
} catch (InvalidOneTimeTokenException e) {
return false;
}
}
}JDBC-based implementation for persistent token storage across application restarts and clusters.
public class JdbcOneTimeTokenService implements OneTimeTokenService{ .api }
Key Methods:
void setDefaultExpiresIn(Duration defaultExpiresIn){ .api }
Sets the default expiration duration for generated tokens (default: 15 minutes).
Database Schema:
CREATE TABLE one_time_tokens (
token_value VARCHAR(255) PRIMARY KEY,
username VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_ott_username ON one_time_tokens(username);
CREATE INDEX idx_ott_expires_at ON one_time_tokens(expires_at);Configuration:
@Configuration
public class JdbcOneTimeTokenConfig {
@Bean
public JdbcOneTimeTokenService jdbcOneTimeTokenService(
DataSource dataSource
) {
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(dataSource);
service.setDefaultExpiresIn(Duration.ofMinutes(30));
return service;
}
@Bean
public DataSource dataSource() {
// Configure your DataSource
return new HikariDataSource(config);
}
}Complete Example:
@Service
public class SecurePasswordResetService {
private final JdbcOneTimeTokenService tokenService;
private final UserDetailsManager userManager;
private final PasswordEncoder passwordEncoder;
public void initiatePasswordReset(String email) {
if (!userManager.userExists(email)) {
// Don't reveal whether user exists
logger.warn("Password reset attempted for non-existent user: {}", email);
return;
}
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(email);
OneTimeToken token = tokenService.generate(request);
String resetUrl = buildResetUrl(token.getTokenValue());
emailService.sendPasswordResetEmail(email, resetUrl, token.getExpiresAt());
logger.info("Password reset initiated for user: {}", email);
}
public void resetPassword(String tokenValue, String newPassword) {
try {
OneTimeToken token = tokenService.consume(tokenValue);
String username = token.getUsername();
UserDetails user = userManager.loadUserByUsername(username);
UserDetails updated = User.builder()
.username(username)
.password(passwordEncoder.encode(newPassword))
.authorities(user.getAuthorities())
.build();
userManager.updateUser(updated);
logger.info("Password reset successful for user: {}", username);
} catch (InvalidOneTimeTokenException e) {
logger.warn("Invalid password reset token: {}", tokenValue);
throw new InvalidResetTokenException("Token is invalid or expired");
}
}
private String buildResetUrl(String token) {
return UriComponentsBuilder
.fromHttpUrl("https://example.com/reset-password")
.queryParam("token", token)
.toUriString();
}
}Reactive in-memory implementation for WebFlux applications.
public class InMemoryReactiveOneTimeTokenService
implements ReactiveOneTimeTokenService{ .api }
Configuration:
@Configuration
public class ReactiveOttConfig {
@Bean
public ReactiveOneTimeTokenService reactiveTokenService() {
return new InMemoryReactiveOneTimeTokenService();
}
}Usage:
@Service
public class ReactiveMagicLinkService {
private final InMemoryReactiveOneTimeTokenService tokenService;
public Mono<String> generateMagicLink(String email) {
return tokenService.generate(new GenerateOneTimeTokenRequest(email))
.map(token -> "https://example.com/login?token=" +
token.getTokenValue());
}
public Mono<Boolean> authenticateWithMagicLink(String tokenValue) {
return tokenService.consume(tokenValue)
.map(token -> true)
.onErrorReturn(InvalidOneTimeTokenException.class, false);
}
}Unauthenticated authentication token containing a one-time token value.
public class OneTimeTokenAuthenticationToken
extends AbstractAuthenticationToken{ .api }
Key Methods:
String getTokenValue(){ .api }
Returns the token value being authenticated.
Example:
OneTimeTokenAuthenticationToken authRequest =
new OneTimeTokenAuthenticationToken("abc123...");Authenticated authentication token after successful OTT validation.
public class OneTimeTokenAuthentication extends AbstractAuthenticationToken{ .api }
Example:
UserDetails user = userDetailsService.loadUserByUsername(token.getUsername());
OneTimeTokenAuthentication authenticated =
new OneTimeTokenAuthentication(user, user.getAuthorities());Authentication provider that validates one-time tokens.
public class OneTimeTokenAuthenticationProvider implements AuthenticationProvider{ .api }
Configuration:
@Configuration
public class OttAuthenticationConfig {
@Bean
public OneTimeTokenAuthenticationProvider ottAuthenticationProvider(
OneTimeTokenService tokenService,
UserDetailsService userDetailsService
) {
return new OneTimeTokenAuthenticationProvider(tokenService, userDetailsService);
}
@Bean
public AuthenticationManager authenticationManager(
OneTimeTokenAuthenticationProvider ottProvider
) {
return new ProviderManager(ottProvider);
}
}Complete Authentication Flow:
@RestController
@RequestMapping("/api/auth")
public class MagicLinkAuthController {
private final OneTimeTokenService tokenService;
private final AuthenticationManager authenticationManager;
@PostMapping("/magic-link/request")
public ResponseEntity<Void> requestMagicLink(@RequestParam String email) {
OneTimeToken token = tokenService.generate(
new GenerateOneTimeTokenRequest(email)
);
String magicLink = buildMagicLink(token.getTokenValue());
emailService.sendMagicLink(email, magicLink);
return ResponseEntity.ok().build();
}
@PostMapping("/magic-link/authenticate")
public ResponseEntity<AuthResponse> authenticateWithMagicLink(
@RequestParam String token
) {
try {
OneTimeTokenAuthenticationToken authRequest =
new OneTimeTokenAuthenticationToken(token);
Authentication authentication =
authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok(new AuthResponse(
authentication.getName(),
generateJwt(authentication)
));
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
private String buildMagicLink(String token) {
return "https://example.com/auth/magic-link?token=" + token;
}
}Reactive authentication manager for one-time tokens.
public class OneTimeTokenReactiveAuthenticationManager
implements ReactiveAuthenticationManager{ .api }
Configuration:
@Configuration
public class ReactiveOttAuthConfig {
@Bean
public OneTimeTokenReactiveAuthenticationManager ottReactiveAuthManager(
ReactiveOneTimeTokenService tokenService,
ReactiveUserDetailsService userDetailsService
) {
return new OneTimeTokenReactiveAuthenticationManager(
tokenService,
userDetailsService
);
}
}Exception thrown when a one-time token is invalid, expired, or already used.
public class InvalidOneTimeTokenException extends AuthenticationException{ .api }
Example Handling:
@Service
public class TokenValidationService {
private final OneTimeTokenService tokenService;
public ValidationResult validateToken(String tokenValue) {
try {
OneTimeToken token = tokenService.consume(tokenValue);
return ValidationResult.success(token.getUsername());
} catch (InvalidOneTimeTokenException e) {
if (e.getMessage().contains("expired")) {
return ValidationResult.expired();
} else if (e.getMessage().contains("not found")) {
return ValidationResult.notFound();
} else {
return ValidationResult.alreadyUsed();
}
}
}
}@Configuration
@EnableWebSecurity
public class MagicLinkSecurityConfig {
@Bean
public JdbcOneTimeTokenService oneTimeTokenService(DataSource dataSource) {
JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(dataSource);
service.setDefaultExpiresIn(Duration.ofMinutes(15));
return service;
}
@Bean
public OneTimeTokenAuthenticationProvider ottProvider(
OneTimeTokenService tokenService,
UserDetailsService userDetailsService
) {
return new OneTimeTokenAuthenticationProvider(tokenService, userDetailsService);
}
@Bean
public AuthenticationManager authenticationManager(
OneTimeTokenAuthenticationProvider ottProvider,
DaoAuthenticationProvider daoProvider
) {
return new ProviderManager(ottProvider, daoProvider);
}
}
@Service
public class MagicLinkAuthenticationService {
private final OneTimeTokenService tokenService;
private final AuthenticationManager authenticationManager;
private final EmailService emailService;
public void sendMagicLink(String email) {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(email);
OneTimeToken token = tokenService.generate(request);
String magicLink = buildMagicLink(token.getTokenValue());
Duration expiresIn = Duration.between(Instant.now(), token.getExpiresAt());
emailService.sendMagicLinkEmail(email, magicLink, expiresIn);
}
public Authentication authenticateWithToken(String tokenValue) {
OneTimeTokenAuthenticationToken authRequest =
new OneTimeTokenAuthenticationToken(tokenValue);
return authenticationManager.authenticate(authRequest);
}
private String buildMagicLink(String token) {
return "https://example.com/login/magic?token=" + token;
}
}
@RestController
@RequestMapping("/auth/magic-link")
public class MagicLinkController {
private final MagicLinkAuthenticationService authService;
@PostMapping("/send")
public ResponseEntity<MessageResponse> sendMagicLink(
@RequestBody @Valid MagicLinkRequest request
) {
authService.sendMagicLink(request.getEmail());
return ResponseEntity.ok(
new MessageResponse("Magic link sent to " + request.getEmail())
);
}
@PostMapping("/verify")
public ResponseEntity<AuthResponse> verifyMagicLink(
@RequestParam String token,
HttpServletRequest request,
HttpServletResponse response
) {
try {
Authentication auth = authService.authenticateWithToken(token);
SecurityContextHolder.getContext().setAuthentication(auth);
// Create session
HttpSession session = request.getSession(true);
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext()
);
return ResponseEntity.ok(new AuthResponse(
auth.getName(),
session.getId()
));
} catch (InvalidOneTimeTokenException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new AuthResponse("Invalid or expired magic link"));
}
}
}org.springframework.security.authentication.ottorg.springframework.security.authentication.ott.reactive