Skip to content

Commit 9003734

Browse files
committed
Polish Default Login Page
Issue gh-17901
1 parent d8c3f25 commit 9003734

File tree

5 files changed

+79
-23
lines changed

5 files changed

+79
-23
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
402402
UserDetails user = PasswordEncodedUser.user();
403403
this.mockMvc.perform(get("/profile").with(user(user)))
404404
.andExpect(status().is3xxRedirection())
405-
.andExpect(redirectedUrl("http://localhost/login?authority=FACTOR_PASSWORD"));
405+
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
406406
this.mockMvc
407407
.perform(post("/ott/generate").param("username", "rod")
408408
.with(user(user))
@@ -418,11 +418,11 @@ void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
418418
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_OTT").build();
419419
this.mockMvc.perform(get("/profile").with(user(user)))
420420
.andExpect(status().is3xxRedirection())
421-
.andExpect(redirectedUrl("http://localhost/login?authority=FACTOR_PASSWORD"));
421+
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
422422
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
423423
this.mockMvc.perform(get("/profile").with(user(user)))
424424
.andExpect(status().is3xxRedirection())
425-
.andExpect(redirectedUrl("http://localhost/login?authority=FACTOR_OTT"));
425+
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
426426
user = PasswordEncodedUser.withUserDetails(user)
427427
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
428428
.build();
@@ -431,14 +431,14 @@ void requestWhenUnauthenticatedThenRequiresTwoSteps() throws Exception {
431431

432432
@Test
433433
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
434-
this.spring.register(MfaDslX509Config.class, UserConfig.class, org.springframework.security.config.annotation.web.configurers.FormLoginConfigurerTests.BasicMfaController.class).autowire();
434+
this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicMfaController.class).autowire();
435435
this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden());
436436
this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build())))
437437
.andExpect(status().isForbidden());
438438
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
439439
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
440440
.andExpect(status().is3xxRedirection())
441-
.andExpect(redirectedUrl("http://localhost/login?authority=FACTOR_PASSWORD"));
441+
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
442442
this.mockMvc
443443
.perform(post("/login").param("username", "rod")
444444
.param("password", "password")
@@ -793,7 +793,8 @@ public <O> O postProcess(O object) {
793793
static class MfaDslConfig {
794794

795795
@Bean
796-
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
796+
SecurityFilterChain filterChain(HttpSecurity http,
797+
AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
797798
// @formatter:off
798799
http
799800
.formLogin(Customizer.withDefaults())
@@ -824,7 +825,8 @@ AuthorizationManagerFactory<?> authz() {
824825
static class MfaDslX509Config {
825826

826827
@Bean
827-
SecurityFilterChain filterChain(HttpSecurity http, AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
828+
SecurityFilterChain filterChain(HttpSecurity http,
829+
AuthorizationManagerFactory<RequestAuthorizationContext> authz) throws Exception {
828830
// @formatter:off
829831
http
830832
.x509(Customizer.withDefaults())

core/src/main/java/org/springframework/security/core/authority/AuthorityUtils.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import java.util.Collections;
2222
import java.util.HashSet;
2323
import java.util.List;
24+
import java.util.Locale;
2425
import java.util.Set;
26+
import java.util.stream.Stream;
2527

2628
import org.springframework.security.core.GrantedAuthority;
2729
import org.springframework.util.Assert;
@@ -39,6 +41,8 @@ public final class AuthorityUtils {
3941

4042
public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
4143

44+
private static String[] KNOWN_PREFIXES = { "ROLE_", "SCOPE_", "FACTOR_" };
45+
4246
private AuthorityUtils() {
4347
}
4448

@@ -66,6 +70,40 @@ public static Set<String> authorityListToSet(Collection<? extends GrantedAuthori
6670
return set;
6771
}
6872

73+
/**
74+
* Return a {@link Stream} containing only the authorities of the given type;
75+
* {@code "ROLE"}, {@code "SCOPE"}, or {@code "FACTOR"}.
76+
* @param type the authority type; {@code "ROLE"}, {@code "SCOPE"}, or
77+
* {@code "FACTOR"}
78+
* @param authorities the list of authorities
79+
* @return a {@link Stream} containing the authorities of the given type
80+
*/
81+
public static Stream<GrantedAuthority> authoritiesOfType(String type, Collection<GrantedAuthority> authorities) {
82+
return authorities.stream().filter((a) -> a.getAuthority().startsWith(type + "_"));
83+
}
84+
85+
/**
86+
* Return the simple name of a {@link GrantedAuthority}, which is its name, less any
87+
* common prefix; that is, {@code ROLE_}, {@code SCOPE_}, or {@code FACTOR_}.
88+
* <p>
89+
* For example, if the authority is {@code ROLE_USER}, then the simple name is
90+
* {@code user}.
91+
* <p>
92+
* If the authority is {@code FACTOR_PASSWORD}, then the simple name is
93+
* {@code password}.
94+
* @param authority the granted authority
95+
* @return the simple name of the authority
96+
*/
97+
public static String getSimpleName(GrantedAuthority authority) {
98+
String name = authority.getAuthority();
99+
for (String prefix : KNOWN_PREFIXES) {
100+
if (name.startsWith(prefix)) {
101+
return name.substring(prefix.length()).toLowerCase(Locale.ROOT);
102+
}
103+
}
104+
return name;
105+
}
106+
69107
/**
70108
* Converts authorities into a List of GrantedAuthority objects.
71109
* @param authorities the authorities to convert

web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.core.log.LogMessage;
3333
import org.springframework.security.core.AuthenticationException;
3434
import org.springframework.security.core.GrantedAuthority;
35+
import org.springframework.security.core.authority.AuthorityUtils;
3536
import org.springframework.security.web.AuthenticationEntryPoint;
3637
import org.springframework.security.web.DefaultRedirectStrategy;
3738
import org.springframework.security.web.PortMapper;
@@ -41,6 +42,7 @@
4142
import org.springframework.security.web.util.RedirectUrlBuilder;
4243
import org.springframework.security.web.util.UrlUtils;
4344
import org.springframework.util.Assert;
45+
import org.springframework.util.CollectionUtils;
4446
import org.springframework.util.StringUtils;
4547
import org.springframework.web.util.UriComponentsBuilder;
4648

@@ -110,15 +112,28 @@ public void afterPropertiesSet() {
110112
* @param exception the exception
111113
* @return the URL (cannot be null or empty; defaults to {@link #getLoginFormUrl()})
112114
*/
115+
@SuppressWarnings("unchecked")
113116
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
114117
AuthenticationException exception) {
118+
Collection<GrantedAuthority> authorities = getAttribute(request, GrantedAuthority.MISSING_AUTHORITIES_ATTRIBUTE,
119+
Collection.class);
120+
if (CollectionUtils.isEmpty(authorities)) {
121+
return getLoginFormUrl();
122+
}
123+
Collection<String> factors = AuthorityUtils.authoritiesOfType("FACTOR", authorities)
124+
.map(AuthorityUtils::getSimpleName)
125+
.toList();
126+
return UriComponentsBuilder.fromUriString(getLoginFormUrl()).queryParam("factor", factors).toUriString();
127+
}
128+
129+
private static <T> @Nullable T getAttribute(HttpServletRequest request, String name, Class<T> clazz) {
115130
Object value = request.getAttribute(GrantedAuthority.MISSING_AUTHORITIES_ATTRIBUTE);
116-
if (value instanceof Collection<?> authorities) {
117-
return UriComponentsBuilder.fromUriString(getLoginFormUrl())
118-
.queryParam("authority", authorities)
119-
.toUriString();
131+
if (value == null) {
132+
return null;
120133
}
121-
return getLoginFormUrl();
134+
String message = String.format("Found %s in %s, but expecting a %s", value.getClass(), name, clazz);
135+
Assert.isInstanceOf(clazz, value, message);
136+
return (T) value;
122137
}
123138

124139
/**

web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
8888

8989
private @Nullable String rememberMeParameter;
9090

91-
private final Collection<String> allowedParameters = List.of("authority");
91+
private final String factorParameter = "factor";
92+
93+
private final Collection<String> allowedParameters = List.of(this.factorParameter);
9294

9395
@SuppressWarnings("NullAway.Init")
9496
private Map<String, String> oauth2AuthenticationUrlToClientName;
@@ -249,29 +251,29 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr
249251
.withRawHtml("passkeyLogin", "");
250252

251253
Predicate<String> wantsAuthority = wantsAuthority(request);
252-
if (wantsAuthority.test("FACTOR_WEBAUTHN")) {
254+
if (wantsAuthority.test("webauthn")) {
253255
builder.withRawHtml("javaScript", renderJavaScript(request, contextPath))
254256
.withRawHtml("passkeyLogin", renderPasskeyLogin());
255257
}
256-
if (wantsAuthority.test("FACTOR_PASSWORD")) {
258+
if (wantsAuthority.test("password")) {
257259
builder.withRawHtml("formLogin",
258260
renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg));
259261
}
260-
if (wantsAuthority.test("FACTOR_OTT")) {
262+
if (wantsAuthority.test("ott")) {
261263
builder.withRawHtml("oneTimeTokenLogin",
262264
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg));
263265
}
264-
if (wantsAuthority.test("FACTOR_AUTHORIZATION_CODE")) {
266+
if (wantsAuthority.test("authorization_code")) {
265267
builder.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath));
266268
}
267-
if (wantsAuthority.test("FACTOR_SAML_RESPONSE")) {
269+
if (wantsAuthority.test("saml_response")) {
268270
builder.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath));
269271
}
270272
return builder.render();
271273
}
272274

273275
private Predicate<String> wantsAuthority(HttpServletRequest request) {
274-
String[] authorities = request.getParameterValues("authority");
276+
String[] authorities = request.getParameterValues(this.factorParameter);
275277
if (authorities == null) {
276278
return (authority) -> true;
277279
}

web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public void generateWhenOneTimeTokenRequestedThenOttForm() throws Exception {
204204
filter.setOneTimeTokenEnabled(true);
205205
filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
206206
MockHttpServletResponse response = new MockHttpServletResponse();
207-
filter.doFilter(TestMockHttpServletRequests.get("/login?authority=FACTOR_OTT").build(), response, this.chain);
207+
filter.doFilter(TestMockHttpServletRequests.get("/login?factor=ott").build(), response, this.chain);
208208
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
209209
assertThat(response.getContentAsString()).contains("""
210210
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate">
@@ -231,9 +231,8 @@ public void generateWhenTwoAuthoritiesRequestedThenBothForms() throws Exception
231231
filter.setOneTimeTokenEnabled(true);
232232
filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
233233
MockHttpServletResponse response = new MockHttpServletResponse();
234-
filter.doFilter(
235-
TestMockHttpServletRequests.get("/login?authority=FACTOR_OTT&authority=FACTOR_PASSWORD").build(),
236-
response, this.chain);
234+
filter.doFilter(TestMockHttpServletRequests.get("/login?factor=ott&factor=password").build(), response,
235+
this.chain);
237236
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
238237
assertThat(response.getContentAsString()).contains("""
239238
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate">

0 commit comments

Comments
 (0)