One Time Token with default configuration

Here is the example of the one time token with default configuration in spring boot.

Steps

  1. User can access any public pages.

  2. User request for login page or try to access any protected page, then login page comes.

  3. User can directly enter username and password and do login or can use one time login.

  4. User enters his username/email and requests for one-time token in the login page .

  5. Spring security validates the user's existence, generates token and sends a one-time token login link to their corresponding email or phone number and shows sent success page.

  6. The user receives the link and by clicking it, he redirects to the link, where he does login with the token provided.

  7. The system validates the token, do the authentication process and allows him to view the secured content.

Security Configuration


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // to encode/ decode password with Bycrypt
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity security) throws Exception {

        return security.authorizeHttpRequests(request ->
                        request
                                .requestMatchers("/public").permitAll()
                                .requestMatchers(HttpMethod.GET, "/ott/sent").permitAll()
                                .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults())
                .oneTimeTokenLogin(Customizer.withDefaults()) // with default setup
                .build();

    }


    // in memory user details service
    @Bean
    public InMemoryUserDetailsManager userDetailsManager() {
        var user = User.withUsername("srv").password(this.passwordEncoder().encode("12345")).build();
        return new InMemoryUserDetailsManager(user);
    }

}
  • /public page is the page that is available to all.

  • /ott/sent is the controller url which will get invoked after successfully token generation.

We also need to configure ott success handler .

One time token success handler

import com.app.token.service.EmailService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;

/**
 * @Author saurabh vaish
 * @Date 02-02-2025
 */
@Profile("basic")
@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 EmailService emailService;


    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {

        var email = checkAndGetEmail(oneTimeToken.getUsername());
        // 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 magicLink = builder.toUriString();

        // send email (JavaMail, SendGrid) or SMS
        System.out.println("Magic Link: " + magicLink);

        var body = """
                Hello, user ! Below you will find your secure link to login !
                %s""".formatted(magicLink);

        try {
            var sendTo = oneTimeToken.getUsername();
            log.info("Sending One Time Token to username: {}", sendTo);
            emailService.sendMail(getEmail(sendTo), "One Time Token Login", body);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("Exception sending email: {}", e.getMessage());
        }

        this.redirectHandler.handle(request, response, oneTimeToken);
    }

    private String checkAndGetEmail(String username) {
        // this would be a database lookup for username
        if(!username.equals("srv")){
            throw new IllegalArgumentException("Username not found");
        }
        log.info("Retrieving email for user: {}", username);
        return "email@email.com"; // email to send mail
    }
}

/ott/sent handler and page

@GetMapping("/ott/sent")
public String ottRedirectPage(){
    return "sent"; // returns sent.jte page to browser
}

sent.jte

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="min-h-screen bg-gray-50">

    <main class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
        <div class="bg-white rounded-lg shadow p-6">
            <h2 class="text-2xl font-semibold mb-4">Magic Link</h2>
            <p class="text-gray-600">
                Please check your email, your One Time Token (OTT) has been sent!
            </p>
        </div>
    </main>
</div>
</body>
</html>

pom.xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- for jte -->
<dependency>
	<groupId>gg.jte</groupId>
	<artifactId>jte</artifactId>
	<version>3.1.12</version>
</dependency>
<dependency>
	<groupId>gg.jte</groupId>
	<artifactId>jte-spring-boot-starter-3</artifactId>
	<version>3.1.12</version>
</dependency>

application.properties

gg.jte.developmentMode=true
gg.jte.templateLocation=src/main/jte
gg.jte.templateSuffix=.jte

Last updated