DEV Community

anand jaisy
anand jaisy

Posted on

One-Time Token passwordless Login in spring security

The Password Paradox:

  • Create a password strong enough to protect state secrets.
  • Forget it instantly.
  • Click 'Forgot Password?'
  • Go through the 5-step authentication ritual.
  • Try to set a new password, only to hear: 'You cannot use a previous password.'

Meanwhile, Chrome is hoarding 47 saved passwords like a digital dragon, yet none of them unlock the one page you actually need right now. 😅"

And just when you think you’ve finally cracked it, the system hits you with:

Your new password must contain at least one uppercase letter, one lowercase letter, one number, one special character, one emoji of existential dread, and the blood of a virgin spreadsheet… and still not be the same as any of the 47 passwords Chrome already knows.

This is one of the reasons passwordless authentication is becoming an attractive approach to modern application security. Instead of asking users to remember yet another password, what if we could authenticate them using a short-lived, one-time token sent to a trusted channel, such as their registered email address?

Setting Up Spring Security

To get started with One-Time Token (OTT) passwordless authentication, we need to add the appropriate Spring Boot dependency to our project.

Depending on the application's requirements, we can use either of the following dependencies:

Option 1: Spring Boot OAuth2 Authorization Server

implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
Enter fullscreen mode Exit fullscreen mode

Option 2: Spring Boot Security

implementation("org.springframework.boot:spring-boot-starter-security")
Enter fullscreen mode Exit fullscreen mode

For this implementation, we will use spring-boot-starter-oauth2-authorization-server, as it provides the Spring Authorization Server functionality built on top of Spring Security.

Note: The choice of dependency depends on the application's architecture. If you only need to implement OTT authentication in an existing Spring Security application, spring-boot-starter-security may be sufficient. The authorization server starter is more appropriate when you are building an OAuth 2.0 or OpenID Connect authorization server.

We invented passwords to protect our data, then made them so complicated that we need another password to recover the first one. Maybe the biggest security vulnerability was asking humans to remember everything! 😅

And here’s the best part: Spring Security makes implementing passwordless authentication much easier than remembering all those passwords! 😄

Instead of reinventing the authentication wheel, we can leverage Spring Security’s built-in One-Time Token (OTT) support and focus on integrating it into our application.

In other words, let Spring Security handle the authentication plumbing while we focus on building features users actually care about.

“Why make users jump through password-reset hoops when Spring Security can help them log in with a one-time token? Less password drama, more actual development!” 🚀

Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http
        .oneTimeTokenLogin(Customizer.withDefaults());
        return http.build();
    }

}
Enter fullscreen mode Exit fullscreen mode

How does it work?

When we enable oneTimeTokenLogin() with its default configuration, Spring Security automatically provides the necessary login pages:

One-Time Token Submission Page: The DefaultOneTimeTokenSubmitPageGeneratingFiltergenerates a default page where users can submit their one-time token to complete authentication.

You enable .oneTimeTokenLogin(Customizer.withDefaults()), start your Spring Boot application, and expect passwordless authentication to work out of the box.

Instead, Spring greets you with a familiar Java experience: a long stack trace and an UnsatisfiedDependencyException. 😅

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'defaultSecurityFilterChain' defined in class path resource [web/security/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'defaultSecurityFilterChain' threw exception with message: A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
Enter fullscreen mode Exit fullscreen mode

Token Delivery
Usually a custom OneTimeTokenGenerationSuccessHandlermust be implemented, responsible for delivering the token to the end user.

@Component
public class OneTimeTokenSenderHandler implements OneTimeTokenGenerationSuccessHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
         String tokenValue = oneTimeToken.getTokenValue();
        // Send the token to the user
        IO.println("One-Time Token generated: " + tokenValue);
    }
}
Enter fullscreen mode Exit fullscreen mode

Customising the OTT Login Page with Angular

When we enable One-Time Token authentication using:

.oneTimeTokenLogin(Customizer.withDefaults())
Enter fullscreen mode Exit fullscreen mode

Spring Security generates default pages for initiating the OTT login process and submitting the token. This is convenient for getting started, but production applications often require a customised login experience that matches the application's branding and frontend architecture.

For example, if our frontend is built with Angular and Angular Material, we probably want users to see our own login component rather than a default Spring Security page.

Fortunately, Spring Security allows us to customise the login page through the loginPage() method in the OTT DSL.

** Configure Spring Security**
We can configure a custom login page and provide a OneTimeTokenGenerationSuccessHandler to handle successful token generation.

.oneTimeTokenLogin(ott ->
                        ott.loginPage("/login")                     .tokenGenerationSuccessHandler(oneTimeTokenSenderHandler)
                );
Enter fullscreen mode Exit fullscreen mode

Here:

loginPage("/login") specifies the custom OTT login page.

tokenGenerationSuccessHandler(oneTimeTokenSenderHandler) defines what happens after Spring Security successfully generates a one-time token. For example, our handler can deliver the token to the user by email.

Important: The login page configuration and token delivery are separate concerns. Customising the login page does not automatically configure email delivery or redirect the user to the Angular application.

<form
      #loginForm="ngForm"
      class="mt-8"
      action="/ott/generate"
      method="POST"
      (ngSubmit)="onSubmit(loginForm)"
    >
      <input type="hidden" name="_csrf" [value]="csrfToken" />
      <div class="flex flex-col">
        <div class="flex flex-col space-y-2">
          <mat-label>Email</mat-label>
          <mat-form-field appearance="outline">
            <input
              name="username"
              type="email"
              matInput
              placeholder="Enter your email"
              required
              email
              #username="ngModel"
              [(ngModel)]="model.username"
              autofocus
            />
            <mat-icon matPrefix>mail</mat-icon>
            @if (username.invalid && (username.dirty || username.touched)) {
              <mat-error>
                @if (username.errors?.['required']) {
                  <span class="text-sm">Email is required</span>
                }
                @if (username.errors?.['email']) {
                  <span class="text-sm">Please enter a valid email</span>
                }
              </mat-error>
            }
          </mat-form-field>
        </div>
      </div>
      <button
        mat-flat-button
        type="submit"
        class="mt-5 w-full font-medium text-base text-white py-3 px-4"
        [disabled]="loginForm.invalid"
      >
        Sign in
      </button>
    </form>
Enter fullscreen mode Exit fullscreen mode

The important detail is the form's endpoint:

action="/ott/generate"
Enter fullscreen mode Exit fullscreen mode

Spring Security processes this request to generate a one-time token. The form submits the user's email address using the username parameter, which is the expected parameter name in the default OTT configuration.

Because this is a POST request, CSRF protection must also be handled correctly. The hidden CSRF field is one way to submit the token, provided the Angular application obtains a valid CSRF token from the backend.


public class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
    private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
    private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
        /*
         * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
         * the CsrfToken when it is rendered in the response body.
         */
        this.xor.handle(request, response, csrfToken);
        /*
         * Render the token value to a cookie by causing the deferred token to be loaded.
         */
        csrfToken.get();
    }

    @Override
    public @Nullable String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
        String headerValue = request.getHeader(csrfToken.getHeaderName());
        if (StringUtils.hasText(headerValue)) {
            return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
        }

        // Fallback to form parameter
        String paramValue = request.getParameter(csrfToken.getParameterName());
        if (StringUtils.hasText(paramValue)) {
            return (StringUtils.hasText(paramValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
        }

        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Redirecting the User After Token Generation

Once Spring Security successfully generates the one-time token, the next step is to deliver it to the user and redirect them to a page where they can enter the code to complete authentication.

To achieve this, we can implement OneTimeTokenGenerationSuccessHandler and delegate the redirect to Spring Security's RedirectOneTimeTokenGenerationSuccessHandler.

public class OneTimeTokenSenderHandler  implements OneTimeTokenGenerationSuccessHandler {
    private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/validate");

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
        String tokenValue = oneTimeToken.getTokenValue();
        // Send the token to the user
        IO.println("One-Time Token generated: " + tokenValue);
        this.redirectHandler.handle(request, response, oneTimeToken);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

One-Time Token passwordless authentication provides a practical alternative to traditional password-based login. By combining Spring Security's built-in OTT support with a custom Angular frontend, we can create a login experience that is straightforward for users while retaining the security controls provided by the Spring Security framework.

The implementation requires only a few key pieces: enabling oneTimeTokenLogin(), configuring a custom login page, implementing a token-generation success handler, delivering the token securely, and providing a page where users can submit it.

Of course, passwordless authentication is not automatically secure just because passwords are no longer involved. Token expiration, single-use enforcement, rate limiting, CSRF protection, and secure delivery remain essential.

The real advantage is that we can reduce password-related friction without having to build an authentication system from scratch.

And perhaps the best part? Users can finally stop asking, “Was my password Spring@123, Spring@1234, or Spring@12345?” 😄

Let Spring Security handle the authentication. Let Angular handle the experience. And let your users get on with what they came to do. 🚀

Top comments (0)