Describe the bug
After a one-time token login, Authentication#getDetails() is null. Form login, HTTP Basic, bearer tokens and
OAuth2 login all set WebAuthenticationDetails (remote address and session id). One-time token login does not, and
there is no supported way to turn it on:
OneTimeTokenAuthenticationConverter creates the OneTimeTokenAuthenticationToken from the token parameter only
and sets no details
(source).
OneTimeTokenAuthenticationFilter authenticates through AbstractAuthenticationProcessingFilter#attemptAuthentication,
which converts the request and authenticates it but never applies the filter's authenticationDetailsSource
(source).
- Because of that,
oneTimeTokenLogin((ott) -> ott.authenticationDetailsSource(...)) is accepted and passed to the
filter
(source),
but changes nothing.
OneTimeTokenAuthenticationProvider already copies the details from the token into its result
(authenticated.setDetails(otpAuthenticationToken.getDetails()),
source),
so it looks like the details were meant to be there.
Impact: anything that reads WebAuthenticationDetails gets null for one-time token logins — for example
AuthenticationSuccessEvent / InteractiveAuthenticationSuccessEvent listeners that record the caller's address.
With multi-factor authentication (form login, then one-time token) the authentication that ends up in the
SecurityContext is the one-time token result, so the address from the password step is not kept either.
To Reproduce
Self-contained program; needs only Spring Security 7.1.1 and spring-test (for the mock request) on the classpath:
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
public class OttDetailsRepro {
public static void main(String[] args) throws Exception {
var users = new InMemoryUserDetailsManager(
User.withUsername("user").password("{noop}password").roles("USER").build());
// Form login: details are populated
var formLogin = new UsernamePasswordAuthenticationFilter(
new ProviderManager(new DaoAuthenticationProvider(users)));
formLogin.setAuthenticationSuccessHandler((request, response, authentication) ->
System.out.println("form login -> details = " + authentication.getDetails()));
var formRequest = post("/login");
formRequest.setParameter("username", "user");
formRequest.setParameter("password", "password");
formLogin.doFilter(formRequest, new MockHttpServletResponse(), new MockFilterChain());
// One-time token login: details are null, even with a details source set explicitly
var tokens = new InMemoryOneTimeTokenService();
var ottLogin = new OneTimeTokenAuthenticationFilter();
ottLogin.setAuthenticationManager(new ProviderManager(new OneTimeTokenAuthenticationProvider(tokens, users)));
ottLogin.setAuthenticationDetailsSource(new WebAuthenticationDetailsSource());
ottLogin.setAuthenticationSuccessHandler((request, response, authentication) ->
System.out.println("one-time token -> details = " + authentication.getDetails()));
var ottRequest = post("/login/ott");
ottRequest.setParameter("token", tokens.generate(new GenerateOneTimeTokenRequest("user")).getTokenValue());
ottLogin.doFilter(ottRequest, new MockHttpServletResponse(), new MockFilterChain());
}
private static MockHttpServletRequest post(String path) {
var request = new MockHttpServletRequest("POST", path);
request.setRemoteAddr("203.0.113.7");
return request;
}
}
Output:
form login -> details = WebAuthenticationDetails [RemoteIpAddress=203.0.113.7, SessionId=null]
one-time token -> details = null
Expected behavior
After a one-time token login, Authentication#getDetails() holds WebAuthenticationDetails, like the other login
mechanisms, and a configured authenticationDetailsSource is used.
A possible fix, matching BasicAuthenticationConverter
(source)
and BearerTokenAuthenticationConverter
(source):
give OneTimeTokenAuthenticationConverter an AuthenticationDetailsSource<HttpServletRequest, ?> (default
WebAuthenticationDetailsSource, with a setter) and set the details on the token it creates.
A broader alternative is for AbstractAuthenticationProcessingFilter#attemptAuthentication to apply its
authenticationDetailsSource to a converted authentication that has no details. That would also make the DSL setting
work, but it changes other converter-based filters too, so I leave that choice to you.
Workaround we use for now:
static AuthenticationConverter oneTimeTokenConverterWithDetails() {
var converter = new OneTimeTokenAuthenticationConverter();
var detailsSource = new WebAuthenticationDetailsSource();
return (request) -> {
Authentication authentication = converter.convert(request);
if (authentication instanceof OneTimeTokenAuthenticationToken token) {
token.setDetails(detailsSource.buildDetails(request));
}
return authentication;
};
}
// http.oneTimeTokenLogin((ott) -> ott.authenticationConverter(oneTimeTokenConverterWithDetails()));
Sample
The program above is the complete reproduction. I can put it in a repository if that helps.
Related: #16856 noticed that otpAuthenticationToken has no details. The fix proposed there (setDetails(user)) would
put the user into the details; the user is already the principal, and details are meant to describe the request.
Environment: Spring Security 7.1.1 (Spring Boot 4.1.1), Java 25. The current main branch has the same code.
Describe the bug
After a one-time token login,
Authentication#getDetails()isnull. Form login, HTTP Basic, bearer tokens andOAuth2 login all set
WebAuthenticationDetails(remote address and session id). One-time token login does not, andthere is no supported way to turn it on:
OneTimeTokenAuthenticationConvertercreates theOneTimeTokenAuthenticationTokenfrom thetokenparameter onlyand sets no details
(source).
OneTimeTokenAuthenticationFilterauthenticates throughAbstractAuthenticationProcessingFilter#attemptAuthentication,which converts the request and authenticates it but never applies the filter's
authenticationDetailsSource(source).
oneTimeTokenLogin((ott) -> ott.authenticationDetailsSource(...))is accepted and passed to thefilter
(source),
but changes nothing.
OneTimeTokenAuthenticationProvideralready copies the details from the token into its result(
authenticated.setDetails(otpAuthenticationToken.getDetails()),source),
so it looks like the details were meant to be there.
Impact: anything that reads
WebAuthenticationDetailsgetsnullfor one-time token logins — for exampleAuthenticationSuccessEvent/InteractiveAuthenticationSuccessEventlisteners that record the caller's address.With multi-factor authentication (form login, then one-time token) the authentication that ends up in the
SecurityContextis the one-time token result, so the address from the password step is not kept either.To Reproduce
Self-contained program; needs only Spring Security 7.1.1 and
spring-test(for the mock request) on the classpath:Output:
Expected behavior
After a one-time token login,
Authentication#getDetails()holdsWebAuthenticationDetails, like the other loginmechanisms, and a configured
authenticationDetailsSourceis used.A possible fix, matching
BasicAuthenticationConverter(source)
and
BearerTokenAuthenticationConverter(source):
give
OneTimeTokenAuthenticationConverteranAuthenticationDetailsSource<HttpServletRequest, ?>(defaultWebAuthenticationDetailsSource, with a setter) and set the details on the token it creates.A broader alternative is for
AbstractAuthenticationProcessingFilter#attemptAuthenticationto apply itsauthenticationDetailsSourceto a converted authentication that has no details. That would also make the DSL settingwork, but it changes other converter-based filters too, so I leave that choice to you.
Workaround we use for now:
Sample
The program above is the complete reproduction. I can put it in a repository if that helps.
Related: #16856 noticed that
otpAuthenticationTokenhas no details. The fix proposed there (setDetails(user)) wouldput the user into the details; the user is already the principal, and details are meant to describe the request.
Environment: Spring Security 7.1.1 (Spring Boot 4.1.1), Java 25. The current
mainbranch has the same code.