diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java index 3c1103270f0..b83663fc82c 100644 --- a/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 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. @@ -18,9 +18,18 @@ import reactor.core.publisher.Mono; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.MessageSourceAccessor; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; import org.springframework.util.Assert; @@ -30,20 +39,30 @@ /** * @author Rob Winch + * @author César Revert * @since 5.0 */ -public class ExceptionTranslationWebFilter implements WebFilter { +public class ExceptionTranslationWebFilter implements WebFilter, MessageSourceAware { private ServerAuthenticationEntryPoint authenticationEntryPoint = new HttpBasicServerAuthenticationEntryPoint(); private ServerAccessDeniedHandler accessDeniedHandler = new HttpStatusServerAccessDeniedHandler( HttpStatus.FORBIDDEN); + private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + + protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(exchange).onErrorResume(AccessDeniedException.class, - (denied) -> exchange.getPrincipal().switchIfEmpty(commenceAuthentication(exchange, denied)) - .flatMap((principal) -> this.accessDeniedHandler.handle(exchange, denied))); + return chain.filter(exchange).onErrorResume(AccessDeniedException.class, (denied) -> exchange.getPrincipal() + .filter((principal) -> (!(principal instanceof Authentication) || (principal instanceof Authentication + && !(this.authenticationTrustResolver.isAnonymous((Authentication) principal))))) + .switchIfEmpty(commenceAuthentication(exchange, + new InsufficientAuthenticationException( + this.messages.getMessage("ExceptionTranslationWebFilter.insufficientAuthentication", + "Full authentication is required to access this resource")))) + .flatMap((principal) -> this.accessDeniedHandler.handle(exchange, denied)).then()); } /** @@ -66,7 +85,28 @@ public void setAuthenticationEntryPoint(ServerAuthenticationEntryPoint authentic this.authenticationEntryPoint = authenticationEntryPoint; } - private Mono commenceAuthentication(ServerWebExchange exchange, AccessDeniedException denied) { + /** + * Sets the authentication trust resolver. + * @param authenticationTrustResolver the authentication trust resolver to use. + * Default is {@link AuthenticationTrustResolverImpl} + * + * @since 5.5 + */ + public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) { + Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must not be null"); + this.authenticationTrustResolver = authenticationTrustResolver; + } + + /** + * @since 5.5 + */ + @Override + public void setMessageSource(MessageSource messageSource) { + Assert.notNull(messageSource, "messageSource cannot be null"); + this.messages = new MessageSourceAccessor(messageSource); + } + + private Mono commenceAuthentication(ServerWebExchange exchange, AuthenticationException denied) { return this.authenticationEntryPoint .commence(exchange, new AuthenticationCredentialsNotFoundException("Not Authenticated", denied)) .then(Mono.empty()); diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java index 847edc204a8..b9ab463880d 100644 --- a/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 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. @@ -30,6 +30,7 @@ import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; @@ -41,6 +42,7 @@ /** * @author Rob Winch + * @author César Revert * @since 5.0 */ @RunWith(MockitoJUnitRunner.class) @@ -49,6 +51,9 @@ public class ExceptionTranslationWebFilterTests { @Mock private Principal principal; + @Mock + private AnonymousAuthenticationToken anonymousPrincipal; + @Mock private ServerWebExchange exchange; @@ -129,6 +134,15 @@ public void filterWhenAccessDeniedExceptionAndAuthenticatedThenHandled() { this.entryPointPublisher.assertWasNotSubscribed(); } + @Test + public void filterWhenAccessDeniedExceptionAndAnonymousAuthenticatedThenHandled() { + given(this.exchange.getPrincipal()).willReturn(Mono.just(this.anonymousPrincipal)); + given(this.chain.filter(this.exchange)).willReturn(Mono.error(new AccessDeniedException("Not Authorized"))); + StepVerifier.create(this.filter.filter(this.exchange, this.chain)).expectComplete().verify(); + this.deniedPublisher.assertWasNotSubscribed(); + this.entryPointPublisher.assertWasSubscribed(); + } + @Test public void setAccessDeniedHandlerWhenNullThenException() { assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAccessDeniedHandler(null)); @@ -139,4 +153,14 @@ public void setAuthenticationEntryPointWhenNullThenException() { assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationEntryPoint(null)); } + @Test + public void setAuthenticationTrustResolver() { + assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationTrustResolver(null)); + } + + @Test + public void setMessageSource() { + assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setMessageSource(null)); + } + }