Browse Source

Use RequiredFactorErrors

Closes gh-18002
pull/17120/merge
Rob Winch 3 months ago
parent
commit
2473378fcd
No known key found for this signature in database
  1. 8
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java
  2. 6
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.java
  3. 2
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java
  4. 6
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java
  5. 6
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java
  6. 2
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/ReauthenticationTests.java
  7. 6
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.kt
  8. 2
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt
  9. 6
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt
  10. 6
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt
  11. 2
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/ReauthenticationTests.kt
  12. 8
      web/src/main/java/org/springframework/security/web/WebAttributes.java
  13. 81
      web/src/main/java/org/springframework/security/web/access/DelegatingMissingAuthorityAccessDeniedHandler.java
  14. 21
      web/src/main/java/org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.java
  15. 10
      web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java
  16. 8
      web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java

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

@ -403,7 +403,7 @@ public class FormLoginConfigurerTests {
UserDetails user = PasswordEncodedUser.user(); UserDetails user = PasswordEncodedUser.user();
this.mockMvc.perform(get("/profile").with(user(user))) this.mockMvc.perform(get("/profile").with(user(user)))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
this.mockMvc this.mockMvc
.perform(post("/ott/generate").param("username", "rod") .perform(post("/ott/generate").param("username", "rod")
.with(user(user)) .with(user(user))
@ -421,13 +421,13 @@ public class FormLoginConfigurerTests {
.build(); .build();
this.mockMvc.perform(get("/profile").with(user(user))) this.mockMvc.perform(get("/profile").with(user(user)))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
user = PasswordEncodedUser.withUserDetails(user) user = PasswordEncodedUser.withUserDetails(user)
.authorities("profile:read", GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY) .authorities("profile:read", GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build(); .build();
this.mockMvc.perform(get("/profile").with(user(user))) this.mockMvc.perform(get("/profile").with(user(user)))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
user = PasswordEncodedUser.withUserDetails(user) user = PasswordEncodedUser.withUserDetails(user)
.authorities("profile:read", GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, .authorities("profile:read", GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
GrantedAuthorities.FACTOR_OTT_AUTHORITY) GrantedAuthorities.FACTOR_OTT_AUTHORITY)
@ -444,7 +444,7 @@ public class FormLoginConfigurerTests {
this.mockMvc.perform(get("/login")).andExpect(status().isOk()); this.mockMvc.perform(get("/login")).andExpect(status().isOk());
this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) this.mockMvc.perform(get("/profile").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
this.mockMvc this.mockMvc
.perform(post("/login").param("username", "rod") .perform(post("/login").param("username", "rod")
.param("password", "password") .param("password", "password")

6
docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.java

@ -69,7 +69,7 @@ public class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
// @formatter:on // @formatter:on
} }
@ -80,7 +80,7 @@ public class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
// @formatter:on // @formatter:on
} }
@ -91,7 +91,7 @@ public class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
// @formatter:on // @formatter:on
} }

2
docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java

@ -58,7 +58,7 @@ public class CustomAuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
// @formatter:on // @formatter:on
} }

6
docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java

@ -69,7 +69,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
// @formatter:on // @formatter:on
} }
@ -80,7 +80,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
// @formatter:on // @formatter:on
} }
@ -91,7 +91,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
// @formatter:on // @formatter:on
} }

6
docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java

@ -69,7 +69,7 @@ public class MultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
// @formatter:on // @formatter:on
} }
@ -80,7 +80,7 @@ public class MultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
// @formatter:on // @formatter:on
} }
@ -91,7 +91,7 @@ public class MultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=password")); .andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
// @formatter:on // @formatter:on
} }

2
docs/src/test/java/org/springframework/security/docs/servlet/authentication/reauthentication/ReauthenticationTests.java

@ -69,7 +69,7 @@ public class ReauthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc.perform(get("/profile")) this.mockMvc.perform(get("/profile"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")); .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
// @formatter:on // @formatter:on
} }

6
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/AuthorizationManagerFactoryTests.kt

@ -68,7 +68,7 @@ class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
// @formatter:on // @formatter:on
} }
@ -81,7 +81,7 @@ class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
// @formatter:on // @formatter:on
} }
@ -94,7 +94,7 @@ class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
// @formatter:on // @formatter:on
} }

2
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt

@ -55,7 +55,7 @@ class CustomAuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(get("/")) this.mockMvc!!.perform(get("/"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login?factor=ott")) .andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
// @formatter:on // @formatter:on
} }

6
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt

@ -68,7 +68,7 @@ class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
// @formatter:on // @formatter:on
} }
@ -81,7 +81,7 @@ class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
// @formatter:on // @formatter:on
} }
@ -94,7 +94,7 @@ class AuthorizationManagerFactoryTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
// @formatter:on // @formatter:on
} }

6
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt

@ -66,7 +66,7 @@ class MultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
// @formatter:on // @formatter:on
} }
@ -78,7 +78,7 @@ class MultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
// @formatter:on // @formatter:on
} }
@ -90,7 +90,7 @@ class MultiFactorAuthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
// @formatter:on // @formatter:on
} }

2
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/reauthentication/ReauthenticationTests.kt

@ -68,7 +68,7 @@ class ReauthenticationTests {
// @formatter:off // @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile")) this.mockMvc!!.perform(MockMvcRequestBuilders.get("/profile"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott")) .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
// @formatter:on // @formatter:on
} }

8
web/src/main/java/org/springframework/security/web/WebAttributes.java

@ -18,7 +18,6 @@ package org.springframework.security.web;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
/** /**
@ -56,15 +55,16 @@ public final class WebAttributes {
+ ".WEB_INVOCATION_PRIVILEGE_EVALUATOR_ATTRIBUTE"; + ".WEB_INVOCATION_PRIVILEGE_EVALUATOR_ATTRIBUTE";
/** /**
* Used to set a {@code Collection} of {@link GrantedAuthority} instances into the * Used to set a {@code Collection} of
* {@link HttpServletRequest}. * {@link org.springframework.security.authorization.RequiredFactorError} instances
* into the {@link HttpServletRequest}.
* <p> * <p>
* Represents what authorities are missing to be authorized for the current request * Represents what authorities are missing to be authorized for the current request
* *
* @since 7.0 * @since 7.0
* @see org.springframework.security.web.access.DelegatingMissingAuthorityAccessDeniedHandler * @see org.springframework.security.web.access.DelegatingMissingAuthorityAccessDeniedHandler
*/ */
public static final String MISSING_AUTHORITIES = WebAttributes.class + ".MISSING_AUTHORITIES"; public static final String REQUIRED_FACTOR_ERRORS = WebAttributes.class + ".REQUIRED_FACTOR_ERRORS ";
private WebAttributes() { private WebAttributes() {
} }

81
web/src/main/java/org/springframework/security/web/access/DelegatingMissingAuthorityAccessDeniedHandler.java

@ -17,11 +17,11 @@
package org.springframework.security.web.access; package org.springframework.security.web.access;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -32,6 +32,10 @@ import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authorization.AuthorityAuthorizationDecision; import org.springframework.security.authorization.AuthorityAuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.FactorAuthorizationDecision;
import org.springframework.security.authorization.RequiredFactor;
import org.springframework.security.authorization.RequiredFactorError;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
@ -93,15 +97,19 @@ public final class DelegatingMissingAuthorityAccessDeniedHandler implements Acce
@Override @Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied) public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied)
throws IOException, ServletException { throws IOException, ServletException {
Collection<GrantedAuthority> authorities = missingAuthorities(denied); List<AuthorityRequiredFactorErrorEntry> authorityErrors = authorityErrors(denied);
for (GrantedAuthority needed : authorities) { for (AuthorityRequiredFactorErrorEntry authorityError : authorityErrors) {
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority()); String requiredAuthority = authorityError.getAuthority();
AuthenticationEntryPoint entryPoint = this.entryPoints.get(requiredAuthority);
if (entryPoint == null) { if (entryPoint == null) {
continue; continue;
} }
this.requestCache.saveRequest(request, response); this.requestCache.saveRequest(request, response);
request.setAttribute(WebAttributes.MISSING_AUTHORITIES, List.of(needed)); RequiredFactorError required = authorityError.getError();
String message = String.format("Missing Authorities %s", List.of(needed)); if (required != null) {
request.setAttribute(WebAttributes.REQUIRED_FACTOR_ERRORS, List.of(required));
}
String message = String.format("Missing Authorities %s", requiredAuthority);
AuthenticationException ex = new InsufficientAuthenticationException(message, denied); AuthenticationException ex = new InsufficientAuthenticationException(message, denied);
entryPoint.commence(request, response, ex); entryPoint.commence(request, response, ex);
return; return;
@ -131,15 +139,39 @@ public final class DelegatingMissingAuthorityAccessDeniedHandler implements Acce
this.requestCache = requestCache; this.requestCache = requestCache;
} }
private Collection<GrantedAuthority> missingAuthorities(AccessDeniedException ex) { private List<AuthorityRequiredFactorErrorEntry> authorityErrors(AccessDeniedException ex) {
AuthorizationDeniedException denied = findAuthorizationDeniedException(ex); AuthorizationDeniedException denied = findAuthorizationDeniedException(ex);
if (denied == null) { if (denied == null) {
return List.of(); return List.of();
} }
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) { AuthorizationResult authorizationResult = denied.getAuthorizationResult();
return List.of(); if (authorizationResult instanceof FactorAuthorizationDecision factorDecision) {
// @formatter:off
return factorDecision.getFactorErrors().stream()
.map((error) -> {
String authority = error.getRequiredFactor().getAuthority();
return new AuthorityRequiredFactorErrorEntry(authority, error);
})
.collect(Collectors.toList());
// @formatter:on
}
if (authorizationResult instanceof AuthorityAuthorizationDecision authorityDecision) {
// @formatter:off
return authorityDecision.getAuthorities().stream()
.map((grantedAuthority) -> {
String authority = grantedAuthority.getAuthority();
if (authority.startsWith("FACTOR_")) {
RequiredFactor required = RequiredFactor.withAuthority(authority).build();
return new AuthorityRequiredFactorErrorEntry(authority, RequiredFactorError.createMissing(required));
}
else {
return new AuthorityRequiredFactorErrorEntry(authority, null);
}
})
.collect(Collectors.toList());
// @formatter:on
} }
return authorization.getAuthorities(); return List.of();
} }
private @Nullable AuthorizationDeniedException findAuthorizationDeniedException(AccessDeniedException ex) { private @Nullable AuthorizationDeniedException findAuthorizationDeniedException(AccessDeniedException ex) {
@ -206,4 +238,33 @@ public final class DelegatingMissingAuthorityAccessDeniedHandler implements Acce
} }
/**
* A mapping of a {@link GrantedAuthority#getAuthority()} to a possibly null
* {@link RequiredFactorError}.
*
* @author Rob Winch
* @since 7.0
*/
private static final class AuthorityRequiredFactorErrorEntry {
private final String authority;
private final @Nullable RequiredFactorError error;
private AuthorityRequiredFactorErrorEntry(String authority, @Nullable RequiredFactorError error) {
Assert.notNull(authority, "authority cannot be null");
this.authority = authority;
this.error = error;
}
private String getAuthority() {
return this.authority;
}
private @Nullable RequiredFactorError getError() {
return this.error;
}
}
} }

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

@ -18,6 +18,7 @@ package org.springframework.security.web.authentication;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import jakarta.servlet.RequestDispatcher; import jakarta.servlet.RequestDispatcher;
@ -31,8 +32,8 @@ import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.security.authorization.RequiredFactorError;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.PortMapper; import org.springframework.security.web.PortMapper;
@ -118,16 +119,22 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) { AuthenticationException exception) {
Collection<GrantedAuthority> authorities = getAttribute(request, WebAttributes.MISSING_AUTHORITIES, Collection<RequiredFactorError> factorErrors = getAttribute(request, WebAttributes.REQUIRED_FACTOR_ERRORS,
Collection.class); Collection.class);
if (CollectionUtils.isEmpty(authorities)) { if (CollectionUtils.isEmpty(factorErrors)) {
return getLoginFormUrl(); return getLoginFormUrl();
} }
Collection<String> factors = authorities.stream() List<String> factorTypes = factorErrors.stream()
.filter((a) -> a.getAuthority().startsWith(FACTOR_PREFIX)) .map((factorError) -> factorError.getRequiredFactor().getAuthority())
.map((a) -> a.getAuthority().substring(FACTOR_PREFIX.length()).toLowerCase(Locale.ROOT)) .map((a) -> a.substring(FACTOR_PREFIX.length()).toLowerCase(Locale.ROOT))
.toList(); .toList();
return UriComponentsBuilder.fromUriString(getLoginFormUrl()).queryParam("factor", factors).toUriString(); List<String> factorReasons = factorErrors.stream()
.map((factorError) -> factorError.isExpired() ? "expired" : "missing")
.toList();
return UriComponentsBuilder.fromUriString(getLoginFormUrl())
.queryParam("factor.type", factorTypes)
.queryParam("factor.reason", factorReasons)
.toUriString();
} }
private static <T> @Nullable T getAttribute(HttpServletRequest request, String name, Class<T> clazz) { private static <T> @Nullable T getAttribute(HttpServletRequest request, String name, Class<T> clazz) {

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

@ -18,10 +18,10 @@ package org.springframework.security.web.authentication.ui;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -88,9 +88,11 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private @Nullable String rememberMeParameter; private @Nullable String rememberMeParameter;
private final String factorParameter = "factor"; private final String factorTypeParameter = "factor.type";
private final Collection<String> allowedParameters = List.of(this.factorParameter); private final String factorReasonParameter = "factor.reason";
private final Set<String> allowedParameters = Set.of(this.factorTypeParameter, this.factorReasonParameter);
@SuppressWarnings("NullAway.Init") @SuppressWarnings("NullAway.Init")
private Map<String, String> oauth2AuthenticationUrlToClientName; private Map<String, String> oauth2AuthenticationUrlToClientName;
@ -281,7 +283,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
} }
private Predicate<String> wantsAuthority(HttpServletRequest request) { private Predicate<String> wantsAuthority(HttpServletRequest request) {
String[] authorities = request.getParameterValues(this.factorParameter); String[] authorities = request.getParameterValues(this.factorTypeParameter);
if (authorities == null) { if (authorities == null) {
return (authority) -> true; return (authority) -> true;
} }

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

@ -204,7 +204,8 @@ public class DefaultLoginPageGeneratingFilterTests {
filter.setOneTimeTokenEnabled(true); filter.setOneTimeTokenEnabled(true);
filter.setOneTimeTokenGenerationUrl("/ott/authenticate"); filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(TestMockHttpServletRequests.get("/login?factor=ott").build(), response, this.chain); filter.doFilter(TestMockHttpServletRequests.get("/login?factor.type=ott&factor.reason=missing").build(),
response, this.chain);
assertThat(response.getContentAsString()).contains("Request a One-Time Token"); assertThat(response.getContentAsString()).contains("Request a One-Time Token");
assertThat(response.getContentAsString()).contains(""" assertThat(response.getContentAsString()).contains("""
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate"> <form id="ott-form" class="login-form" method="post" action="/ott/authenticate">
@ -231,8 +232,9 @@ public class DefaultLoginPageGeneratingFilterTests {
filter.setOneTimeTokenEnabled(true); filter.setOneTimeTokenEnabled(true);
filter.setOneTimeTokenGenerationUrl("/ott/authenticate"); filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(TestMockHttpServletRequests.get("/login?factor=ott&factor=password").build(), response, filter.doFilter(TestMockHttpServletRequests
this.chain); .get("/login?factor.type=ott&factor.type=password&factor.reason=missing&factor.reason=missing")
.build(), response, this.chain);
assertThat(response.getContentAsString()).contains("Request a One-Time Token"); assertThat(response.getContentAsString()).contains("Request a One-Time Token");
assertThat(response.getContentAsString()).contains(""" assertThat(response.getContentAsString()).contains("""
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate"> <form id="ott-form" class="login-form" method="post" action="/ott/authenticate">

Loading…
Cancel
Save