Here is the example of the one time token in spring security with custom configuration.
This is the custom one time token implementation with some own configurations.
AppUser-
Entity that stores the user basic information.
OneTimeToken-
Entity that stores one time token information, like name, expiration etc.
CustomUserDetailsService-
UserDetailsService implementation that fetches users from that database and returns a UserDetails (I) which has the implementation AppUser.
SecurityConfig-
The default submit page url and token generation urls are the same, but after token validation we are directing to /login/ott/success that returns the secure page.
JDBCTokenGeneratorService-
This is the custom token generation service, which is responsible for generating tokens and validating them.
It is inspired from original JDBCTokenGeneratorService, just the expiry time has been increased from 5 to 15 minutes.
It also has the cron jobs that clears token automatically.
OneTimeTokenSuccessHandler-
In this class, after token generation token are being sent to username, for that first user is being fetched from the database using a custom user details service
pom.xml
application.properties
AppUser entity
OneTimeToken entity
Security Configuration
OneTimeTokenSuccessHandler
UserDetailsService
rest of the details like controller and html pages are the same as previous one.
@Profile("custom")
@Configuration
@EnableWebSecurity
@Log4j2
@RequiredArgsConstructor
public class SecurityConfig {
private final JdbcOperations jdbcOperations;
private final String [] permitUrl = {"/error","/exp","/public","/ott/sent","/login/ott"};
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain configure(HttpSecurity security) throws Exception {
return security.authorizeHttpRequests(request ->
request
.requestMatchers(permitUrl).permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults()) // default form login
.oneTimeTokenLogin(ott->
ott
// default is fine, uses SimpleUrlAuthenticationFailureHandler , that checks token and prepare authentication object
// by default only gets username but we can configure Authentication Manger and UserDetailsService to get more details
// .authenticationProvider() // with different or custom authentication provider for ott verification
// default login page for ott, can be changed to custom one but need to handle csrf
.defaultSubmitPageUrl("/login/ott") // token submit page , default is "/login/ott" can be changed to custom one
// .showDefaultSubmitPage(true) // sets the value to show submit page or not
// default is fine , no need to change but can be changed to custom one
.tokenGeneratingUrl("/ott/generate") // url that specifies how token will get generated with , default one time token service - [InMemory or Jdbc]
.loginProcessingUrl("/login/ott/success") // specifies the url that process login request
// taking jdbc token service instead of in memory
.tokenService(jbdcTokenService()) // used to specify TokenGenerationService - InMemory Or JdbcTokenService , default is {@link InMemoryOneTimeTokenService} with default expiry of 5 minutes
// .authenticationFailureHandler() // specifies the things when token is not valid, defaults to /login?error - SimpleUrlAuthenticationFailureHandler
// configures the success handler overrides from default that redirect to previous page, it always redirect to /home
.authenticationSuccessHandler(successHandler()) // specifies the success handler - default SavedRequestAwareAuthenticationSuccessHandler
)
// .csrf(AbstractHttpConfigurer::disable) // csrf needed as spring uses form login for ott
.build();
}
// authentication manager that uses DaoAuthenticationProvider to get user info from database
@Bean
public AuthenticationManager authenticationManager(CustomUserDetailsService customUserDetailsService) throws Exception {
var provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(customUserDetailsService);
return new ProviderManager(provider);
}
// custom token generation service
private OneTimeTokenService jbdcTokenService() {
// return new JdbcOneTimeTokenService(jdbcOperations); // default JdbcOneTimeTokenService with default expiry of 5 minutes
return new JDBCTokenGeneratorService(jdbcOperations); // custom OneTimeTokenService with default expiry of 15 minutes
}
// authentication success handler that redirects to home page after login
private AuthenticationSuccessHandler successHandler() {
return new SimpleUrlAuthenticationSuccessHandler("/home");
}
}
@Profile("custom")
@Component
@Log4j2
@RequiredArgsConstructor
public class OneTimeTokenSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ott success handler that uses redirect to /ott/sent page
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
private final AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler("/exp?message=Username not found");
private final EmailService emailService;
private final CustomUserDetailsService userDetailsService;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
var email = "";
try{
email = checkAndGetEmail(oneTimeToken.getUsername()); // validated and get email
}catch (Exception e){
log.error("Exception getting email for user: {}", oneTimeToken.getUsername());
failureHandler.onAuthenticationFailure(request, response, new UsernameNotFoundException("Exception getting email for user"));
return;
}
// creates a logging processing url with path /login/ott and with the token
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue());
String tokenLink = builder.toUriString();
System.out.println("Token Link: " + magicLink);
var body = """
Hello, %s! Below you will find your secure link to login !
%s""".formatted(oneTimeToken.getUserName(),tokenLink);
try {
var sendTo = oneTimeToken.getUsername();
log.info("Sending One Time Token to username: {}", sendTo);
// sending mail to user
emailService.sendMail(email, "One Time Token Login", body,false);
} catch (Exception e) {
e.printStackTrace();
log.error("Exception sending email: {}", e.getMessage());
}
// redirects to token sent page
this.redirectHandler.handle(request, response, oneTimeToken);
}
// getting user by user name from user details service
private String checkAndGetEmail(String username) {
log.info("Retrieving email for user: {}", username);
var user = (AppUser)userDetailsService.loadUserByUsername(username);
return user.getEmail();
}
}
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final AppUserRepository appUserRepository;
// fetches user from database and returns spring security UserDetails
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return appUserRepository.findByUsernameIgnoreCase(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}