- Security
- A
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.
Example
Suppose we have a resource page that we want to access. Let's start with the traditional spring security login form:
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:
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):
Click the "Sign in" button and get access to the coveted resource:
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:
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 OneTimeTokenService
– InMemoryOneTimeTokenService
(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
:
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.
Write comment