Skip to content

Commit 74dcab3

Browse files
Address PR Feedback
- Add final to implementations - Remove OneTimeTokenAuthenticationFilter and use AuthenticationFilter directly - Align method names in OneTimeToken - Create OneTimeTokenAuthenticationRequestSuccessHandler for a better control of the strategy used when a successful ott authentication request is made, defaults to RedirectOneTimeTokenAuthenticationRequestSuccessHandler
1 parent 0c46878 commit 74dcab3

File tree

15 files changed

+215
-69
lines changed

15 files changed

+215
-69
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
3333
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3434
import org.springframework.security.web.authentication.logout.LogoutFilter;
35-
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
3635
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
3736
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3837
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;

config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
import org.springframework.security.web.access.intercept.AuthorizationFilter;
2828
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
2929
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
30+
import org.springframework.security.web.authentication.AuthenticationFilter;
3031
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3132
import org.springframework.security.web.authentication.logout.LogoutFilter;
32-
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
3333
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationRequestFilter;
3434
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
3535
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
@@ -99,7 +99,6 @@ final class FilterOrderRegistration {
9999
this.filterToOrder.put(
100100
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
101101
order.next());
102-
put(OneTimeTokenAuthenticationFilter.class, order.next());
103102
put(UsernamePasswordAuthenticationFilter.class, order.next());
104103
order.next(); // gh-8105
105104
put(DefaultLoginPageGeneratingFilter.class, order.next());
@@ -111,6 +110,7 @@ final class FilterOrderRegistration {
111110
"org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter",
112111
order.next());
113112
put(BasicAuthenticationFilter.class, order.next());
113+
put(AuthenticationFilter.class, order.next());
114114
put(RequestCacheAwareFilter.class, order.next());
115115
put(SecurityContextHolderAwareRequestFilter.class, order.next());
116116
put(JaasApiIntegrationFilter.class, order.next());

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,18 @@
4040
import org.springframework.security.core.userdetails.UserDetailsService;
4141
import org.springframework.security.web.authentication.AuthenticationConverter;
4242
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
43+
import org.springframework.security.web.authentication.AuthenticationFilter;
4344
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
4445
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
4546
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
4647
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
47-
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
4848
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationRequestFilter;
49+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationRequestSuccessHandler;
50+
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenAuthenticationRequestSuccessHandler;
4951
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
5052
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
53+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
54+
import org.springframework.security.web.context.SecurityContextRepository;
5155
import org.springframework.security.web.csrf.CsrfToken;
5256
import org.springframework.util.Assert;
5357
import org.springframework.util.StringUtils;
@@ -79,7 +83,8 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
7983

8084
private String authenticationRequestUrl = "/ott/authenticate";
8185

82-
private String authenticationRequestRedirectUrl = "/login/ott";
86+
private OneTimeTokenAuthenticationRequestSuccessHandler authenticationRequestSuccessHandler = new RedirectOneTimeTokenAuthenticationRequestSuccessHandler(
87+
"/login/ott");
8388

8489
private AuthenticationProvider authenticationProvider;
8590

@@ -118,18 +123,27 @@ public void configure(H http) {
118123

119124
private void configureOttAuthenticationFilter(H http) {
120125
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
121-
OneTimeTokenAuthenticationFilter oneTimeTokenAuthenticationFilter = new OneTimeTokenAuthenticationFilter(
122-
authenticationManager, this.authenticationConverter);
126+
AuthenticationFilter oneTimeTokenAuthenticationFilter = new AuthenticationFilter(authenticationManager,
127+
this.authenticationConverter);
128+
oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http));
123129
oneTimeTokenAuthenticationFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
124130
oneTimeTokenAuthenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
125131
oneTimeTokenAuthenticationFilter.setSuccessHandler(this.authenticationSuccessHandler);
126132
http.addFilter(postProcess(oneTimeTokenAuthenticationFilter));
127133
}
128134

135+
private SecurityContextRepository getSecurityContextRepository(H http) {
136+
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
137+
if (securityContextRepository != null) {
138+
return securityContextRepository;
139+
}
140+
return new HttpSessionSecurityContextRepository();
141+
}
142+
129143
private void configureOttAuthenticationRequestFilter(H http) {
130144
OneTimeTokenAuthenticationRequestFilter authenticationRequestFilter = new OneTimeTokenAuthenticationRequestFilter(
131145
getOneTimeTokenService(http), getOneTimeTokenSender(http));
132-
authenticationRequestFilter.setRedirectUrl(this.authenticationRequestRedirectUrl);
146+
authenticationRequestFilter.setAuthenticationRequestSuccessHandler(this.authenticationRequestSuccessHandler);
133147
authenticationRequestFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.authenticationRequestUrl));
134148
http.addFilter(postProcess(authenticationRequestFilter));
135149
}
@@ -177,15 +191,15 @@ public OneTimeTokenLoginConfigurer<H> authenticationRequestUrl(String authentica
177191
}
178192

179193
/**
180-
* Specifies the URL that the user-agent will be redirected after a successful
181-
* One-Time Token authentication. Defaults to {@code POST /login/ott}. If you are
182-
* using the default submit page make sure that you also configure
183-
* {@link #submitPageUrl(String)} to this same URL.
184-
* @param authenticationRequestRedirectUrl
194+
* Specifies strategy to be used for successful one-time token authentication
195+
* requests. By default, a redirect will be performed to {@code POST /login/ott} using
196+
* the {@link RedirectOneTimeTokenAuthenticationRequestSuccessHandler}.
197+
* @param authenticationRequestSuccessHandler
185198
*/
186-
public OneTimeTokenLoginConfigurer<H> authenticationRequestRedirectUrl(String authenticationRequestRedirectUrl) {
187-
Assert.hasText(authenticationRequestRedirectUrl, "authenticationRequestRedirectUrl cannot be null or empty");
188-
this.authenticationRequestRedirectUrl = authenticationRequestRedirectUrl;
199+
public OneTimeTokenLoginConfigurer<H> authenticationRequestSuccessHandler(
200+
OneTimeTokenAuthenticationRequestSuccessHandler authenticationRequestSuccessHandler) {
201+
Assert.notNull(authenticationRequestSuccessHandler, "authenticationRequestSuccessHandler cannot be null");
202+
this.authenticationRequestSuccessHandler = authenticationRequestSuccessHandler;
189203
return this;
190204
}
191205

config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.springframework.security.core.userdetails.UserDetailsService;
3535
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
3636
import org.springframework.security.web.SecurityFilterChain;
37+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
38+
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenAuthenticationRequestSuccessHandler;
3739
import org.springframework.test.web.servlet.MockMvc;
3840

3941
import static org.assertj.core.api.Assertions.assertThatException;
@@ -58,7 +60,7 @@ void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception {
5860
this.mvc.perform(post("/ott/authenticate").param("username", "user").with(csrf()))
5961
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
6062

61-
String token = TestOneTimeTokenSender.lastToken.getToken();
63+
String token = TestOneTimeTokenSender.lastToken.getTokenValue();
6264

6365
this.mvc.perform(post("/login/ott").param("token", token).with(csrf()))
6466
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
@@ -68,12 +70,12 @@ void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception {
6870
void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() throws Exception {
6971
this.spring.register(OneTimeTokenDifferentAuthenticationUrlsConfig.class).autowire();
7072
this.mvc.perform(post("/authenticationrequesturl").param("username", "user").with(csrf()))
71-
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
73+
.andExpectAll(status().isFound(), redirectedUrl("/redirected"));
7274

73-
String token = TestOneTimeTokenSender.lastToken.getToken();
75+
String token = TestOneTimeTokenSender.lastToken.getTokenValue();
7476

7577
this.mvc.perform(post("/loginprocessingurl").param("token", token).with(csrf()))
76-
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
78+
.andExpectAll(status().isFound(), redirectedUrl("/authenticated"), authenticated());
7779
}
7880

7981
@Test
@@ -82,7 +84,7 @@ void oneTimeTokenWhenCorrectTokenUsedTwiceThenSecondTimeFails() throws Exception
8284
this.mvc.perform(post("/ott/authenticate").param("username", "user").with(csrf()))
8385
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
8486

85-
String token = TestOneTimeTokenSender.lastToken.getToken();
87+
String token = TestOneTimeTokenSender.lastToken.getTokenValue();
8688

8789
this.mvc.perform(post("/login/ott").param("token", token).with(csrf()))
8890
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
@@ -169,7 +171,9 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
169171
)
170172
.oneTimeTokenLogin((ott) -> ott
171173
.authenticationRequestUrl("/authenticationrequesturl")
174+
.authenticationRequestSuccessHandler(new RedirectOneTimeTokenAuthenticationRequestSuccessHandler("/redirected"))
172175
.loginProcessingUrl("/loginprocessingurl")
176+
.authenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/authenticated"))
173177
);
174178
// @formatter:on
175179
return http.build();

core/src/main/java/org/springframework/security/authentication/ott/DefaultOneTimeToken.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public DefaultOneTimeToken(String token, String username, Instant expireAt) {
4444
}
4545

4646
@Override
47-
public String getToken() {
47+
public String getTokenValue() {
4848
return this.token;
4949
}
5050

@@ -53,7 +53,7 @@ public String getUsername() {
5353
return this.username;
5454
}
5555

56-
public Instant getExpireAt() {
56+
public Instant getExpiresAt() {
5757
return this.expireAt;
5858
}
5959

core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
* @author Marcus da Coregio
3535
* @since 6.4
3636
*/
37-
public class InMemoryOneTimeTokenService implements OneTimeTokenService {
37+
public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
3838

3939
private final Map<String, OneTimeToken> oneTimeTokenByToken = new ConcurrentHashMap<>();
4040

@@ -72,7 +72,7 @@ private void cleanExpiredTokensIfNeeded() {
7272
}
7373

7474
private boolean isExpired(OneTimeToken ott) {
75-
return this.clock.instant().isAfter(ott.getExpireAt());
75+
return this.clock.instant().isAfter(ott.getExpiresAt());
7676
}
7777

7878
void setClock(Clock clock) {

core/src/main/java/org/springframework/security/authentication/ott/OneTimeToken.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
public interface OneTimeToken {
2828

2929
/**
30-
* @return the one-time token, never {@code null}
30+
* @return the one-time token value, never {@code null}
3131
*/
32-
String getToken();
32+
String getTokenValue();
3333

3434
/**
3535
* @return the username associated with this token, never {@code null}
@@ -39,6 +39,6 @@ public interface OneTimeToken {
3939
/**
4040
* @return the expiration time of the token
4141
*/
42-
Instant getExpireAt();
42+
Instant getExpiresAt();
4343

4444
}

core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* @author Marcus da Coregio
3232
* @since 6.4
3333
*/
34-
public class OneTimeTokenAuthenticationProvider implements AuthenticationProvider {
34+
public final class OneTimeTokenAuthenticationProvider implements AuthenticationProvider {
3535

3636
private final OneTimeTokenService oneTimeTokenService;
3737

core/src/test/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenServiceTests.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class InMemoryOneTimeTokenServiceTests {
4343
void generateThenTokenValueShouldBeValidUuidAndProvidedUsernameIsUsed() {
4444
OneTimeTokenAuthenticationRequest request = new OneTimeTokenAuthenticationRequest("user");
4545
OneTimeToken oneTimeToken = this.oneTimeTokenService.generate(request);
46-
assertThatNoException().isThrownBy(() -> UUID.fromString(oneTimeToken.getToken()));
46+
assertThatNoException().isThrownBy(() -> UUID.fromString(oneTimeToken.getTokenValue()));
4747
assertThat(request.getUsername()).isEqualTo("user");
4848
}
4949

@@ -58,18 +58,20 @@ void consumeWhenTokenDoesNotExistsThenNull() {
5858
void consumeWhenTokenExistsThenReturnItself() {
5959
OneTimeTokenAuthenticationRequest request = new OneTimeTokenAuthenticationRequest("user");
6060
OneTimeToken generated = this.oneTimeTokenService.generate(request);
61-
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(generated.getToken());
61+
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
62+
generated.getTokenValue());
6263
OneTimeToken consumed = this.oneTimeTokenService.consume(authenticationToken);
63-
assertThat(consumed.getToken()).isEqualTo(generated.getToken());
64+
assertThat(consumed.getTokenValue()).isEqualTo(generated.getTokenValue());
6465
assertThat(consumed.getUsername()).isEqualTo(generated.getUsername());
65-
assertThat(consumed.getExpireAt()).isEqualTo(generated.getExpireAt());
66+
assertThat(consumed.getExpiresAt()).isEqualTo(generated.getExpiresAt());
6667
}
6768

6869
@Test
6970
void consumeWhenTokenIsExpiredThenReturnNull() {
7071
OneTimeTokenAuthenticationRequest request = new OneTimeTokenAuthenticationRequest("user");
7172
OneTimeToken generated = this.oneTimeTokenService.generate(request);
72-
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(generated.getToken());
73+
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
74+
generated.getTokenValue());
7375
Clock tenMinutesFromNow = Clock.fixed(Instant.now().plus(10, ChronoUnit.MINUTES), ZoneOffset.UTC);
7476
this.oneTimeTokenService.setClock(tenMinutesFromNow);
7577
OneTimeToken consumed = this.oneTimeTokenService.consume(authenticationToken);
@@ -88,12 +90,12 @@ void generateWhenMoreThan100TokensThenClearExpired() {
8890

8991
assertThat(toExpire)
9092
.extracting(
91-
(token) -> this.oneTimeTokenService.consume(new OneTimeTokenAuthenticationToken(token.getToken())))
93+
(token) -> this.oneTimeTokenService.consume(new OneTimeTokenAuthenticationToken(token.getTokenValue())))
9294
.containsOnlyNulls();
9395

9496
assertThat(toKeep)
9597
.extracting(
96-
(token) -> this.oneTimeTokenService.consume(new OneTimeTokenAuthenticationToken(token.getToken())))
98+
(token) -> this.oneTimeTokenService.consume(new OneTimeTokenAuthenticationToken(token.getTokenValue())))
9799
.noneMatch(Objects::isNull);
98100
// @formatter:on
99101
}

docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ In summary, One-Time Tokens (OTT) provide a way to authenticate users without ad
2828

2929
The One-Time Token Login works in two major steps.
3030

31-
1. User request a token by submitting their user identifier, usually the username, and the token is delivered to them, via e-mail, SMS, Magic Link, etc.
31+
1. User requests a token by submitting their user identifier, usually the username, and the token is delivered to them, often as a Magic Link, via e-mail, SMS, etc.
3232
2. User submits the token to the application and, if valid, the user gets logged in.
3333

3434
[[default-pages]]

0 commit comments

Comments
 (0)