Make Public Missing Authority AccessDeniedHandler
Issue gh-17934
This commit is contained in:
+41
-198
@@ -16,40 +16,26 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
|
||||
import org.springframework.security.web.access.DelegatingMissingAuthorityAccessDeniedHandler;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.ThrowableAnalyzer;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Adds exception handling for Spring Security related exceptions to an application. All
|
||||
@@ -94,7 +80,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entryPoints = new LinkedHashMap<>();
|
||||
private final DelegatingMissingAuthorityAccessDeniedHandler.Builder missingAuthoritiesHandlerBuilder = DelegatingMissingAuthorityAccessDeniedHandler
|
||||
.builder();
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
@@ -146,6 +133,37 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
|
||||
* invoked for the provided missing {@link GrantedAuthority}.
|
||||
* @param entryPoint the {@link AuthenticationEntryPoint} to use for the given
|
||||
* {@code authority}
|
||||
* @param authority the authority
|
||||
* @return the {@link ExceptionHandlingConfigurer} for further customizations
|
||||
* @since 7.0
|
||||
*/
|
||||
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
|
||||
String authority) {
|
||||
this.missingAuthoritiesHandlerBuilder.addEntryPointFor(entryPoint, authority);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
|
||||
* invoked for the provided missing {@link GrantedAuthority}.
|
||||
* @param entryPoint a consumer of a
|
||||
* {@link DelegatingAuthenticationEntryPoint.Builder} to use for the given
|
||||
* {@code authority}
|
||||
* @param authority the authority
|
||||
* @return the {@link ExceptionHandlingConfigurer} for further customizations
|
||||
* @since 7.0
|
||||
*/
|
||||
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(
|
||||
Consumer<DelegatingAuthenticationEntryPoint.Builder> entryPoint, String authority) {
|
||||
this.missingAuthoritiesHandlerBuilder.addEntryPointFor(entryPoint, authority);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationEntryPoint} to be used.
|
||||
*
|
||||
@@ -189,26 +207,6 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
|
||||
RequestMatcher preferredMatcher, String authority) {
|
||||
this.defaultEntryPointMappings.put(preferredMatcher, entryPoint);
|
||||
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> byMatcher = this.entryPoints.get(authority);
|
||||
if (byMatcher == null) {
|
||||
byMatcher = new LinkedHashMap<>();
|
||||
}
|
||||
byMatcher.put(preferredMatcher, entryPoint);
|
||||
this.entryPoints.put(authority, byMatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
|
||||
String authority) {
|
||||
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> byMatcher = new LinkedHashMap<>();
|
||||
byMatcher.put(AnyRequestMatcher.INSTANCE, entryPoint);
|
||||
this.entryPoints.put(authority, byMatcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any explicitly configured {@link AuthenticationEntryPoint}
|
||||
* @return
|
||||
@@ -269,22 +267,10 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
|
||||
AccessDeniedHandler defaults = createDefaultAccessDeniedHandler(http);
|
||||
if (this.entryPoints.isEmpty()) {
|
||||
return defaults;
|
||||
}
|
||||
Map<String, AccessDeniedHandler> deniedHandlers = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entry : this.entryPoints
|
||||
.entrySet()) {
|
||||
AuthenticationEntryPoint entryPoint = entryPointFrom(entry.getValue());
|
||||
AuthenticationEntryPointAccessDeniedHandlerAdapter deniedHandler = new AuthenticationEntryPointAccessDeniedHandlerAdapter(
|
||||
entryPoint);
|
||||
RequestCache requestCache = http.getSharedObject(RequestCache.class);
|
||||
if (requestCache != null) {
|
||||
deniedHandler.setRequestCache(requestCache);
|
||||
}
|
||||
deniedHandlers.put(entry.getKey(), deniedHandler);
|
||||
}
|
||||
return new AuthenticationFactorDelegatingAccessDeniedHandler(deniedHandlers, defaults);
|
||||
DelegatingMissingAuthorityAccessDeniedHandler deniedHandler = this.missingAuthoritiesHandlerBuilder.build();
|
||||
deniedHandler.setRequestCache(getRequestCache(http));
|
||||
deniedHandler.setDefaultAccessDeniedHandler(defaults);
|
||||
return deniedHandler;
|
||||
}
|
||||
|
||||
private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
|
||||
@@ -299,29 +285,10 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
}
|
||||
|
||||
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
|
||||
AuthenticationEntryPoint defaults = entryPointFrom(this.defaultEntryPointMappings);
|
||||
if (this.entryPoints.isEmpty()) {
|
||||
return defaults;
|
||||
}
|
||||
Map<String, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entry : this.entryPoints
|
||||
.entrySet()) {
|
||||
entryPoints.put(entry.getKey(), entryPointFrom(entry.getValue()));
|
||||
}
|
||||
return new AuthenticationFactorDelegatingAuthenticationEntryPoint(entryPoints, defaults);
|
||||
}
|
||||
|
||||
private AuthenticationEntryPoint entryPointFrom(
|
||||
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints) {
|
||||
if (entryPoints.isEmpty()) {
|
||||
if (this.defaultEntryPoint == null) {
|
||||
return new Http403ForbiddenEntryPoint();
|
||||
}
|
||||
if (entryPoints.size() == 1) {
|
||||
return entryPoints.values().iterator().next();
|
||||
}
|
||||
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
|
||||
entryPoint.setDefaultEntryPoint(entryPoints.values().iterator().next());
|
||||
return entryPoint;
|
||||
return this.defaultEntryPoint.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,128 +307,4 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return new HttpSessionRequestCache();
|
||||
}
|
||||
|
||||
private static final class AuthenticationFactorDelegatingAuthenticationEntryPoint
|
||||
implements AuthenticationEntryPoint {
|
||||
|
||||
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
|
||||
|
||||
private final Map<String, AuthenticationEntryPoint> entryPoints;
|
||||
|
||||
private final AuthenticationEntryPoint defaults;
|
||||
|
||||
private AuthenticationFactorDelegatingAuthenticationEntryPoint(
|
||||
Map<String, AuthenticationEntryPoint> entryPoints, AuthenticationEntryPoint defaults) {
|
||||
this.entryPoints = new LinkedHashMap<>(entryPoints);
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
|
||||
throws IOException, ServletException {
|
||||
Collection<GrantedAuthority> authorization = authorizationRequest(ex);
|
||||
entryPoint(authorization).commence(request, response, ex);
|
||||
}
|
||||
|
||||
private AuthenticationEntryPoint entryPoint(Collection<GrantedAuthority> authorities) {
|
||||
if (authorities == null) {
|
||||
return this.defaults;
|
||||
}
|
||||
for (GrantedAuthority needed : authorities) {
|
||||
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority());
|
||||
if (entryPoint != null) {
|
||||
return entryPoint;
|
||||
}
|
||||
}
|
||||
return this.defaults;
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> authorizationRequest(Exception ex) {
|
||||
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
|
||||
AuthorizationDeniedException denied = (AuthorizationDeniedException) this.throwableAnalyzer
|
||||
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
|
||||
if (denied == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
|
||||
return List.of();
|
||||
}
|
||||
return authorization.getAuthorities();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class AuthenticationEntryPointAccessDeniedHandlerAdapter implements AccessDeniedHandler {
|
||||
|
||||
private final AuthenticationEntryPoint entryPoint;
|
||||
|
||||
private RequestCache requestCache = new NullRequestCache();
|
||||
|
||||
private AuthenticationEntryPointAccessDeniedHandlerAdapter(AuthenticationEntryPoint entryPoint) {
|
||||
this.entryPoint = entryPoint;
|
||||
}
|
||||
|
||||
void setRequestCache(RequestCache requestCache) {
|
||||
Assert.notNull(requestCache, "requestCache cannot be null");
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied)
|
||||
throws IOException, ServletException {
|
||||
AuthenticationException ex = new InsufficientAuthenticationException("access denied", denied);
|
||||
this.requestCache.saveRequest(request, response);
|
||||
this.entryPoint.commence(request, response, ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
|
||||
|
||||
private final Map<String, AccessDeniedHandler> deniedHandlers;
|
||||
|
||||
private final AccessDeniedHandler defaults;
|
||||
|
||||
private AuthenticationFactorDelegatingAccessDeniedHandler(Map<String, AccessDeniedHandler> deniedHandlers,
|
||||
AccessDeniedHandler defaults) {
|
||||
this.deniedHandlers = new LinkedHashMap<>(deniedHandlers);
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
|
||||
throws IOException, ServletException {
|
||||
Collection<GrantedAuthority> authorization = authorizationRequest(ex);
|
||||
deniedHandler(authorization).handle(request, response, ex);
|
||||
}
|
||||
|
||||
private AccessDeniedHandler deniedHandler(Collection<GrantedAuthority> authorities) {
|
||||
if (authorities == null) {
|
||||
return this.defaults;
|
||||
}
|
||||
for (GrantedAuthority needed : authorities) {
|
||||
AccessDeniedHandler deniedHandler = this.deniedHandlers.get(needed.getAuthority());
|
||||
if (deniedHandler != null) {
|
||||
return deniedHandler;
|
||||
}
|
||||
}
|
||||
return this.defaults;
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> authorizationRequest(Exception ex) {
|
||||
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
|
||||
AuthorizationDeniedException denied = (AuthorizationDeniedException) this.throwableAnalyzer
|
||||
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
|
||||
if (denied == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
|
||||
return List.of();
|
||||
}
|
||||
return authorization.getAuthorities();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-1
@@ -233,7 +233,10 @@ public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
initDefaultLoginFilter(http);
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_PASSWORD");
|
||||
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint();
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(entryPoint, requestMatcher),
|
||||
"FACTOR_PASSWORD");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -192,8 +192,10 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (exceptionHandling == null) {
|
||||
return;
|
||||
}
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(this.authenticationEntryPoint),
|
||||
preferredMatcher);
|
||||
AuthenticationEntryPoint entryPoint = postProcess(this.authenticationEntryPoint);
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(entryPoint, preferredMatcher);
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(entryPoint, preferredMatcher),
|
||||
"FACTOR_PASSWORD");
|
||||
}
|
||||
|
||||
private void registerDefaultLogoutSuccessHandler(B http, RequestMatcher preferredMatcher) {
|
||||
|
||||
+5
-2
@@ -27,12 +27,14 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
|
||||
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||
@@ -155,8 +157,9 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
public void init(H http) throws Exception {
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),
|
||||
"FACTOR_WEBAUTHN");
|
||||
AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint("/login");
|
||||
exceptions.defaultAuthenticationEntryPointFor(
|
||||
(ep) -> ep.addEntryPointFor(entryPoint, AnyRequestMatcher.INSTANCE), "FACTOR_WEBAUTHN");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -184,8 +184,9 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
|
||||
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE,
|
||||
"FACTOR_X509");
|
||||
AuthenticationEntryPoint forbidden = new Http403ForbiddenEntryPoint();
|
||||
exceptions.defaultAuthenticationEntryPointFor(
|
||||
(ep) -> ep.addEntryPointFor(forbidden, AnyRequestMatcher.INSTANCE), "FACTOR_X509");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-5
@@ -373,10 +373,6 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
http.authenticationProvider(new OidcAuthenticationRequestChecker());
|
||||
}
|
||||
this.initDefaultLoginFilter(http);
|
||||
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_AUTHORIZATION_CODE");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -561,11 +557,18 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
|
||||
new NegatedRequestMatcher(defaultLoginPageMatcher), formLoginNotEnabled);
|
||||
// @formatter:off
|
||||
return DelegatingAuthenticationEntryPoint.builder()
|
||||
AuthenticationEntryPoint loginEntryPoint = DelegatingAuthenticationEntryPoint.builder()
|
||||
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
|
||||
.defaultEntryPoint(getAuthenticationEntryPoint())
|
||||
.build();
|
||||
// @formatter:on
|
||||
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(loginEntryPoint, requestMatcher),
|
||||
"FACTOR_AUTHORIZATION_CODE");
|
||||
}
|
||||
return loginEntryPoint;
|
||||
}
|
||||
|
||||
private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {
|
||||
|
||||
+2
-4
@@ -259,10 +259,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
if (authenticationProvider != null) {
|
||||
http.authenticationProvider(authenticationProvider);
|
||||
}
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(this.authenticationEntryPoint, "FACTOR_BEARER");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -331,6 +327,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
RequestMatcher preferredMatcher = new OrRequestMatcher(
|
||||
Arrays.asList(this.requestMatcher, X_REQUESTED_WITH, restNotHtmlMatcher, allMatcher));
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(this.authenticationEntryPoint, preferredMatcher);
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(
|
||||
(ep) -> ep.addEntryPointFor(this.authenticationEntryPoint, preferredMatcher), "FACTOR_BEARER");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-62
@@ -16,15 +16,10 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers.ott;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -42,13 +37,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractAu
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.FormPostRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
@@ -67,7 +57,6 @@ import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AbstractHttpConfigurer} for One-Time Token Login.
|
||||
@@ -149,9 +138,10 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
intiDefaultLoginFilter(http);
|
||||
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
AuthenticationEntryPoint entryPoint = new PostAuthenticationEntryPoint(
|
||||
this.tokenGeneratingUrl + "?username={u}", Map.of("u", Authentication::getName));
|
||||
exceptions.defaultAuthenticationEntryPointFor(entryPoint, "FACTOR_OTT");
|
||||
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint();
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(entryPoint, requestMatcher),
|
||||
"FACTOR_OTT");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,52 +400,4 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
return this.context;
|
||||
}
|
||||
|
||||
private static final class PostAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private final String entryPointUri;
|
||||
|
||||
private final Map<String, Function<Authentication, String>> params;
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private RedirectStrategy redirectStrategy = new FormPostRedirectStrategy();
|
||||
|
||||
private PostAuthenticationEntryPoint(String entryPointUri,
|
||||
Map<String, Function<Authentication, String>> params) {
|
||||
this.entryPointUri = entryPointUri;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
Authentication authentication = getAuthentication(authException);
|
||||
Assert.notNull(authentication, "could not find authentication in order to perform post");
|
||||
Map<String, String> params = this.params.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, (entry) -> entry.getValue().apply(authentication)));
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(this.entryPointUri);
|
||||
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
if (csrf != null) {
|
||||
builder.queryParam(csrf.getParameterName(), csrf.getToken());
|
||||
}
|
||||
String entryPointUrl = builder.build(false).expand(params).toUriString();
|
||||
this.redirectStrategy.sendRedirect(request, response, entryPointUrl);
|
||||
}
|
||||
|
||||
private Authentication getAuthentication(AuthenticationException authException) {
|
||||
Authentication authentication = authException.getAuthenticationRequest();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return authentication;
|
||||
}
|
||||
authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return authentication;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-5
@@ -305,10 +305,6 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (this.authenticationManager == null) {
|
||||
registerDefaultAuthenticationProvider(http);
|
||||
}
|
||||
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_SAML_RESPONSE");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,11 +344,18 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
|
||||
new NegatedRequestMatcher(defaultLoginPageMatcher));
|
||||
// @formatter:off
|
||||
return DelegatingAuthenticationEntryPoint.builder()
|
||||
AuthenticationEntryPoint loginEntryPoint = DelegatingAuthenticationEntryPoint.builder()
|
||||
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
|
||||
.defaultEntryPoint(getAuthenticationEntryPoint())
|
||||
.build();
|
||||
// @formatter:on
|
||||
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptions != null) {
|
||||
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
|
||||
exceptions.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(loginEntryPoint, requestMatcher),
|
||||
"FACTOR_SAML_RESPONSE");
|
||||
}
|
||||
return loginEntryPoint;
|
||||
}
|
||||
|
||||
private void setAuthenticationRequestRepository(B http,
|
||||
|
||||
+4
-6
@@ -64,7 +64,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
@@ -79,7 +78,6 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -423,8 +421,8 @@ public class FormLoginConfigurerTests {
|
||||
.andExpect(redirectedUrl("http://localhost/login"));
|
||||
user = PasswordEncodedUser.withUserDetails(user).authorities("profile:read", "FACTOR_PASSWORD").build();
|
||||
this.mockMvc.perform(get("/profile").with(user(user)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("/ott/generate")));
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login"));
|
||||
user = PasswordEncodedUser.withUserDetails(user)
|
||||
.authorities("profile:read", "FACTOR_PASSWORD", "FACTOR_OTT")
|
||||
.build();
|
||||
@@ -433,8 +431,8 @@ public class FormLoginConfigurerTests {
|
||||
|
||||
@Test
|
||||
void requestWhenUnauthenticatedX509ThenRequiresTwoSteps() throws Exception {
|
||||
this.spring.register(MfaDslX509Config.class, UserConfig.class, org.springframework.security.config.annotation.web.configurers.FormLoginConfigurerTests.BasicMfaController.class).autowire();
|
||||
this.mockMvc.perform(get("/profile")).andExpect(status().isForbidden());
|
||||
this.spring.register(MfaDslX509Config.class, UserConfig.class, BasicMfaController.class).autowire();
|
||||
this.mockMvc.perform(get("/profile")).andExpect(status().is3xxRedirection());
|
||||
this.mockMvc.perform(get("/profile").with(user(User.withUsername("rod").authorities("profile:read").build())))
|
||||
.andExpect(status().isForbidden());
|
||||
this.mockMvc.perform(get("/login")).andExpect(status().isOk());
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.access;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.ThrowableAnalyzer;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
|
||||
/**
|
||||
* An {@link AccessDeniedHandler} that adapts {@link AuthenticationEntryPoint}s based on
|
||||
* missing {@link GrantedAuthority}s. These authorities are specified in an
|
||||
* {@link AuthorityAuthorizationDecision} inside an {@link AuthorizationDeniedException}.
|
||||
*
|
||||
* <p>
|
||||
* This is helpful in adaptive authentication scenarios where an
|
||||
* {@link org.springframework.security.authorization.AuthorizationManager} indicates
|
||||
* additional authorities needed to access a given resource.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For example, if an
|
||||
* {@link org.springframework.security.authorization.AuthorizationManager} states that to
|
||||
* access the home page, the user needs the {@code FACTOR_OTT} authority, then this
|
||||
* handler can be configured in the following way to redirect to the one-time-token login
|
||||
* page:
|
||||
* </p>
|
||||
*
|
||||
* <code>
|
||||
* AccessDeniedHandler handler = DelegatingMissingAuthorityAccessDeniedHandler.builder()
|
||||
* .authorities("FACTOR_OTT").commence(new LoginUrlAuthenticationEntryPoint("/login"))
|
||||
* .authorities("FACTOR_PASSWORD")...
|
||||
* .build();
|
||||
* </code>
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 7.0
|
||||
* @see AuthorizationDeniedException
|
||||
* @see AuthorityAuthorizationDecision
|
||||
* @see org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer
|
||||
*/
|
||||
public final class DelegatingMissingAuthorityAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
|
||||
|
||||
private final Map<String, AuthenticationEntryPoint> entryPoints;
|
||||
|
||||
private RequestCache requestCache = new NullRequestCache();
|
||||
|
||||
private AccessDeniedHandler defaultAccessDeniedHandler = new AccessDeniedHandlerImpl();
|
||||
|
||||
private DelegatingMissingAuthorityAccessDeniedHandler(Map<String, AuthenticationEntryPoint> entryPoints) {
|
||||
this.entryPoints = entryPoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied)
|
||||
throws IOException, ServletException {
|
||||
Collection<GrantedAuthority> authorities = missingAuthorities(denied);
|
||||
AuthenticationEntryPoint entryPoint = entryPoint(authorities);
|
||||
if (entryPoint == null) {
|
||||
this.defaultAccessDeniedHandler.handle(request, response, denied);
|
||||
return;
|
||||
}
|
||||
this.requestCache.saveRequest(request, response);
|
||||
AuthenticationException ex = new InsufficientAuthenticationException("missing authorities", denied);
|
||||
entryPoint.commence(request, response, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link AccessDeniedHandler} for {@link AccessDeniedException}s that this
|
||||
* handler doesn't support. By default, this uses {@link AccessDeniedHandlerImpl}.
|
||||
* @param defaultAccessDeniedHandler the default {@link AccessDeniedHandler} to use
|
||||
*/
|
||||
public void setDefaultAccessDeniedHandler(AccessDeniedHandler defaultAccessDeniedHandler) {
|
||||
this.defaultAccessDeniedHandler = defaultAccessDeniedHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RequestCache} to remember the current request.
|
||||
* <p>
|
||||
* Uses {@link NullRequestCache} by default
|
||||
* </p>
|
||||
* @param requestCache the {@link RequestCache} to use
|
||||
*/
|
||||
public void setRequestCache(RequestCache requestCache) {
|
||||
this.requestCache = requestCache;
|
||||
}
|
||||
|
||||
private @Nullable AuthenticationEntryPoint entryPoint(Collection<GrantedAuthority> authorities) {
|
||||
for (GrantedAuthority needed : authorities) {
|
||||
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority());
|
||||
if (entryPoint == null) {
|
||||
continue;
|
||||
}
|
||||
return entryPoint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> missingAuthorities(AccessDeniedException ex) {
|
||||
AuthorizationDeniedException denied = findAuthorizationDeniedException(ex);
|
||||
if (denied == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
|
||||
return List.of();
|
||||
}
|
||||
return authorization.getAuthorities();
|
||||
}
|
||||
|
||||
private @Nullable AuthorizationDeniedException findAuthorizationDeniedException(AccessDeniedException ex) {
|
||||
if (ex instanceof AuthorizationDeniedException denied) {
|
||||
return denied;
|
||||
}
|
||||
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
|
||||
return (AuthorizationDeniedException) this.throwableAnalyzer
|
||||
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for configuring the set of authority/entry-point pairs
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 7.0
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final Map<String, DelegatingAuthenticationEntryPoint.Builder> entryPointByRequestMatcherByAuthority = new LinkedHashMap<>();
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
DelegatingAuthenticationEntryPoint.Builder entryPointBuilder(String authority) {
|
||||
return this.entryPointByRequestMatcherByAuthority.computeIfAbsent(authority,
|
||||
(k) -> DelegatingAuthenticationEntryPoint.builder());
|
||||
}
|
||||
|
||||
void entryPoint(String authority, AuthenticationEntryPoint entryPoint) {
|
||||
DelegatingAuthenticationEntryPoint.Builder builder = DelegatingAuthenticationEntryPoint.builder()
|
||||
.addEntryPointFor(entryPoint, AnyRequestMatcher.INSTANCE);
|
||||
this.entryPointByRequestMatcherByAuthority.put(authority, builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind these authorities to the given {@link AuthenticationEntryPoint}
|
||||
* @param entryPoint the {@link AuthenticationEntryPoint} for the given
|
||||
* authorities
|
||||
* @param authorities the authorities
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder addEntryPointFor(AuthenticationEntryPoint entryPoint, String... authorities) {
|
||||
for (String authority : authorities) {
|
||||
Builder.this.entryPoint(authority, entryPoint);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind these authorities to the given {@link AuthenticationEntryPoint}
|
||||
* @param entryPoint a consumer to configure the underlying
|
||||
* {@link DelegatingAuthenticationEntryPoint}
|
||||
* @param authorities the authorities
|
||||
* @return the {@link Builder} for further configurations
|
||||
*/
|
||||
public Builder addEntryPointFor(Consumer<DelegatingAuthenticationEntryPoint.Builder> entryPoint,
|
||||
String... authorities) {
|
||||
for (String authority : authorities) {
|
||||
entryPoint.accept(Builder.this.entryPointBuilder(authority));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public DelegatingMissingAuthorityAccessDeniedHandler build() {
|
||||
Map<String, AuthenticationEntryPoint> entryPointByAuthority = new LinkedHashMap<>();
|
||||
for (String authority : this.entryPointByRequestMatcherByAuthority.keySet()) {
|
||||
entryPointByAuthority.put(authority, this.entryPointByRequestMatcherByAuthority.get(authority).build());
|
||||
}
|
||||
return new DelegatingMissingAuthorityAccessDeniedHandler(entryPointByAuthority);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.access;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DelegatingMissingAuthorityAccessDeniedHandlerTests {
|
||||
|
||||
DelegatingMissingAuthorityAccessDeniedHandler.Builder builder;
|
||||
|
||||
MockHttpServletRequest request;
|
||||
|
||||
MockHttpServletResponse response;
|
||||
|
||||
@Mock
|
||||
AuthenticationEntryPoint factorEntryPoint;
|
||||
|
||||
@Mock
|
||||
AccessDeniedHandler defaultAccessDeniedHandler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.builder = DelegatingMissingAuthorityAccessDeniedHandler.builder();
|
||||
this.builder.addEntryPointFor(this.factorEntryPoint, "FACTOR");
|
||||
this.request = new MockHttpServletRequest();
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenKnownAuthorityThenCommences() throws Exception {
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("FACTOR"));
|
||||
verify(this.factorEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenUnknownAuthorityThenDefaultCommences() throws Exception {
|
||||
DelegatingMissingAuthorityAccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.setDefaultAccessDeniedHandler(this.defaultAccessDeniedHandler);
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("ROLE_USER"));
|
||||
verify(this.defaultAccessDeniedHandler).handle(any(), any(), any());
|
||||
verifyNoInteractions(this.factorEntryPoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoAuthoritiesFoundThenDefaultCommences() throws Exception {
|
||||
DelegatingMissingAuthorityAccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.setDefaultAccessDeniedHandler(this.defaultAccessDeniedHandler);
|
||||
accessDeniedHandler.handle(this.request, this.response, new AccessDeniedException("access denied"));
|
||||
verify(this.defaultAccessDeniedHandler).handle(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleAuthoritiesThenFirstMatchCommences() throws Exception {
|
||||
AuthenticationEntryPoint passwordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
this.builder.addEntryPointFor(passwordEntryPoint, "PASSWORD");
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("PASSWORD", "FACTOR"));
|
||||
verify(passwordEntryPoint).commence(any(), any(), any());
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("FACTOR", "PASSWORD"));
|
||||
verify(this.factorEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomRequestCacheThenUses() throws Exception {
|
||||
RequestCache requestCache = mock(RequestCache.class);
|
||||
DelegatingMissingAuthorityAccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.setRequestCache(requestCache);
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("FACTOR"));
|
||||
verify(requestCache).saveRequest(any(), any());
|
||||
verify(this.factorEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenKnownAuthorityButNoRequestMatchThenCommences() throws Exception {
|
||||
AuthenticationEntryPoint passwordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
RequestMatcher xhr = new RequestHeaderRequestMatcher("X-Requested-With");
|
||||
this.builder.addEntryPointFor((ep) -> ep.addEntryPointFor(passwordEntryPoint, xhr), "PASSWORD");
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("PASSWORD"));
|
||||
verify(passwordEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleEntryPointsThenFirstRequestMatchCommences() throws Exception {
|
||||
AuthenticationEntryPoint basicPasswordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
AuthenticationEntryPoint formPasswordEntryPoint = mock(AuthenticationEntryPoint.class);
|
||||
RequestMatcher xhr = new RequestHeaderRequestMatcher("X-Requested-With");
|
||||
this.builder.addEntryPointFor(
|
||||
(ep) -> ep.addEntryPointFor(basicPasswordEntryPoint, xhr).defaultEntryPoint(formPasswordEntryPoint),
|
||||
"PASSWORD");
|
||||
AccessDeniedHandler accessDeniedHandler = this.builder.build();
|
||||
accessDeniedHandler.handle(this.request, this.response, missingAuthorities("PASSWORD"));
|
||||
verify(formPasswordEntryPoint).commence(any(), any(), any());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("X-Requested-With", "XmlHttpRequest");
|
||||
accessDeniedHandler.handle(request, this.response, missingAuthorities("PASSWORD"));
|
||||
verify(basicPasswordEntryPoint).commence(any(), any(), any());
|
||||
}
|
||||
|
||||
AuthorizationDeniedException missingAuthorities(String... authorities) {
|
||||
Collection<GrantedAuthority> granted = AuthorityUtils.createAuthorityList(authorities);
|
||||
AuthorityAuthorizationDecision decision = new AuthorityAuthorizationDecision(false, granted);
|
||||
return new AuthorizationDeniedException("access denied", decision);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user