Magic links now in Spring Security

The concept of magic links is not new, but for a long time developers did not have a reliable solution with a full-fledged community that would allow them to quickly and easily implement magic links in their applications. And finally, such a feature has appeared in spring security. Let's take a closer look at what we got, what problems remain, and what is planned to be implemented.

One-Time Tokens

To support magic links, the framework has added so-called "one-time tokens". The main idea of such tokens is that authorization can be obtained with it exactly once. In addition, a unique one-time token, as befits a token, has a limited lifespan, by default it is 5 minutes. The link itself is generated based on this token and sent to the user, for example, by email.


Magic links in Spring Security: protection and convenience

Example

Suppose we have a resource page that we want to access. Let's start with the traditional spring security login form:


Spring Security: the magic of links for secure access

Now we have an additional login option - "Request a One-Time Token". Let's try to enter our hypothetical email, for example, mail@example.com. We receive a notification:


Innovations in Spring Security: magic links

If the user exists, a magic link has been sent to them. In our example, it is http://localhost:8080/login/ott?token=baecd038-a7d1-4f5b-8c07-bc4f974c5281. When following this link, we will get a token input window (although it is not necessary):


Security and magic: magic links in Spring Security

Click the "Sign in" button and get access to the coveted resource:


Spring Security: new features with magic links

In the best traditions of spring security, everything in this process is subject to fairly flexible customization. Let's see how we can set it all up in the application code.

Code

The starter for connecting spring security remains spring-boot-starter-security, nothing changes here. It is not difficult to guess that for the most part everything comes down to configuring SecurityFilterChain.

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests((auth) ->
                        auth.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                                .permitAll()
                                .requestMatchers("/page")
                                .permitAll()
                                .anyRequest()
                                .authenticated()
                )
                .formLogin(Customizer.withDefaults())
                .oneTimeTokenLogin(Customizer.withDefaults())
                .build();
    }

The new thing here is oneTimeTokenLogin. This is our one-time token login process. With oneTimeTokenLogin enabled, formLogin will have the ability to log in with a one-time token. Our page resource is enough to mark as permitAll. We still won't be able to access it just like that.

For the feature to work correctly, we also need to define userDetailsService:

    @Bean
    InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("mail@example.com")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

The new component GenerateOneTimeTokenFilter is responsible for generating the token:


Magic links in Spring Security: a new level of protection

Of course, the default url /ott/generate can be replaced with any other through the oneTimeTokenLogin setting. The filter delegates the generation of the one-time token to the OneTimeTokenService component (generate method). At the moment there are two implementations of OneTimeTokenServiceInMemoryOneTimeTokenService (tokens are stored in a hash table) and JdbcOneTimeTokenService for a clustered environment. There is currently a small bug in JdbcOneTimeTokenService, I think this PR will be included in the next minor version of spring security.

After the token is generated, OneTimeTokenGenerationSuccessHandler starts working. The implementation of this component should create the magic link itself and, for example, send it to the user's email.

@Component
public final class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {

    private final MailSender mailSender;

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    public MagicLinkGenerationSuccessHandler(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
        String magicLink = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
                .replacePath(request.getContextPath())
                .replaceQuery(null)
                .fragment(null)
                .path("/login/ott")
                .queryParam("token", oneTimeToken.getTokenValue())
                .toUriString();

        this.mailSender.send(oneTimeToken.getUsername(), "Your Spring Security One Time Token",
                "Use the following link to sign in into the application: " + magicLink);
        this.redirectStrategy.sendRedirect(request, response, "/page");
    }
}

After sending the magic link, we get redirected to a protected resource, where we will see a message that the link has been sent to the email.

When following the link that contains /login/ott and the token parameter – the generated value in OneTimeTokenService, we will get to the filter DefaultOneTimeTokenSubmitPageGeneratingFilter:


Spring Security: magic links for a secure web application

This filter provides a default page where we see a form with a one-time token input. In fact, this form is only for demonstration purposes and has no practical meaning in displaying the token. Much more interesting for us is the configuration of AuthenticationFilter – in the diagram, I indicated only the components specific to oneTimeTokenLogin. For those familiar with the architecture of spring security, understanding the configuration of AuthenticationFilter will not be difficult: OneTimeTokenAuthenticationConverter extracts the token parameter from the request and creates an authentication object OneTimeTokenAuthenticationToken. ProviderManager delegates the process to the provider OneTimeTokenAuthenticationProvider, which in turn receives the previously generated one-time token (object OneTimeToken) from OneTimeTokenService and completes the process – creates an authenticated object OneTimeTokenAuthenticationToken.

A full example can be found in my GitHub repository. The reactive implementation of the feature is also available.

What's next

At a minimum, you need to fix the bugs that arise as the feature is used. In addition, there are questions about customizing the one-time token itself, now this can be done only by implementing your own OneTimeTokenService, which is inconvenient and somewhat cumbersome. The solution already exists - it is support GenerateOneTimeTokenRequestResolver, a typical spring security approach. Also, the implementation R2dbcReactiveOneTimeTokenService is in limbo.

In the following articles, we will discuss WebAuthn support in spring security, which has been long awaited and finally appeared. Friends, subscribe to my Telegram channel, there you will find a lot of interesting content on the topic of InfoSec.

Comments