diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index ccb2caa077..188b9d761a 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -65,6 +65,7 @@ import org.springframework.security.oauth2.client.web.server.authentication.OAut import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler; import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint; import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint; @@ -90,6 +91,7 @@ import org.springframework.security.web.server.authorization.AuthorizationWebFil import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; +import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; import org.springframework.security.web.server.context.ReactorContextWebFilter; import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; @@ -230,6 +232,9 @@ public class ServerHttpSecurity { private ServerAccessDeniedHandler accessDeniedHandler; + private List + defaultAccessDeniedHandlers = new ArrayList<>(); + private List webFilters = new ArrayList<>(); private ApplicationContext context; @@ -687,6 +692,9 @@ public class ServerHttpSecurity { * Configures OAuth2 Resource Server Support */ public class OAuth2ResourceServerSpec { + private BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); + private BearerTokenServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler(); + private JwtSpec jwt; public JwtSpec jwt() { @@ -752,9 +760,10 @@ public class ServerHttpSecurity { new ServerBearerTokenAuthenticationConverter(); this.bearerTokenServerWebExchangeMatcher.setBearerTokenConverter(bearerTokenConverter); + registerDefaultAccessDeniedHandler(http); + registerDefaultAuthenticationEntryPoint(http); registerDefaultCsrfOverride(http); - BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); ReactiveJwtDecoder jwtDecoder = getJwtDecoder(); JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager( jwtDecoder); @@ -763,9 +772,6 @@ public class ServerHttpSecurity { oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint)); http - .exceptionHandling() - .authenticationEntryPoint(entryPoint) - .and() .addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); } @@ -776,6 +782,28 @@ public class ServerHttpSecurity { return this.jwtDecoder; } + private void registerDefaultAccessDeniedHandler(ServerHttpSecurity http) { + if ( http.exceptionHandling != null ) { + http.defaultAccessDeniedHandlers.add( + new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry( + this.bearerTokenServerWebExchangeMatcher, + new BearerTokenServerAccessDeniedHandler() + ) + ); + } + } + + private void registerDefaultAuthenticationEntryPoint(ServerHttpSecurity http) { + if ( http.exceptionHandling != null ) { + http.defaultEntryPoints.add( + new DelegateEntry( + this.bearerTokenServerWebExchangeMatcher, + new BearerTokenServerAuthenticationEntryPoint() + ) + ); + } + } + private void registerDefaultCsrfOverride(ServerHttpSecurity http) { if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) { http @@ -1033,8 +1061,10 @@ public class ServerHttpSecurity { exceptionTranslationWebFilter.setAuthenticationEntryPoint( authenticationEntryPoint); } - if (this.accessDeniedHandler != null) { - exceptionTranslationWebFilter.setAccessDeniedHandler(this.accessDeniedHandler); + ServerAccessDeniedHandler accessDeniedHandler = getAccessDeniedHandler(); + if (accessDeniedHandler != null) { + exceptionTranslationWebFilter.setAccessDeniedHandler( + accessDeniedHandler); } this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION); this.authorizeExchange.configure(this); @@ -1077,6 +1107,20 @@ public class ServerHttpSecurity { return result; } + private ServerAccessDeniedHandler getAccessDeniedHandler() { + if (this.accessDeniedHandler != null || this.defaultAccessDeniedHandlers.isEmpty()) { + return this.accessDeniedHandler; + } + if (this.defaultAccessDeniedHandlers.size() == 1) { + return this.defaultAccessDeniedHandlers.get(0).getAccessDeniedHandler(); + } + ServerWebExchangeDelegatingServerAccessDeniedHandler result = + new ServerWebExchangeDelegatingServerAccessDeniedHandler(this.defaultAccessDeniedHandlers); + result.setDefaultAccessDeniedHandler(this.defaultAccessDeniedHandlers + .get(this.defaultAccessDeniedHandlers.size() - 1).getAccessDeniedHandler()); + return result; + } + /** * Creates a new instance. * @return the new {@link ServerHttpSecurity} instance diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index db35656753..b3e99f2981 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -171,6 +171,17 @@ public class OAuth2ResourceServerSpecTests { .expectStatus().isOk(); } + @Test + public void getWhenTokenHasInsufficientScopeThenReturnsInsufficientScope() { + this.spring.register(DenyAllConfig.class, RootController.class).autowire(); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isForbidden() + .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"insufficient_scope\"")); + } + @Test public void postWhenMissingTokenThenReturnsForbidden() { this.spring.register(PublicKeyConfig.class, RootController.class).autowire(); @@ -248,22 +259,17 @@ public class OAuth2ResourceServerSpecTests { SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { // @formatter:off http + .authorizeExchange() + .anyExchange().hasAuthority("SCOPE_message:read") + .and() .oauth2ResourceServer() .jwt() - .publicKey(this.publicKey()); + .publicKey(publicKey()); // @formatter:on + return http.build(); } - - RSAPublicKey publicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { - String modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797"; - String exponent = "65537"; - - RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent)); - KeyFactory factory = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) factory.generatePublic(spec); - } } @EnableWebFlux @@ -318,6 +324,25 @@ public class OAuth2ResourceServerSpecTests { } } + @EnableWebFlux + @EnableWebFluxSecurity + static class DenyAllConfig { + @Bean + SecurityWebFilterChain authorization(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange() + .anyExchange().denyAll() + .and() + .oauth2ResourceServer() + .jwt() + .publicKey(publicKey()); + // @formatter:on + + return http.build(); + } + } + @RestController static class RootController { @GetMapping @@ -331,6 +356,16 @@ public class OAuth2ResourceServerSpecTests { } } + + private static RSAPublicKey publicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + String modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797"; + String exponent = "65537"; + + RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent)); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) factory.generatePublic(spec); + } + private GenericWebApplicationContext autowireWebServerGenericWebApplicationContext() { GenericWebApplicationContext context = new GenericWebApplicationContext(); context.registerBean("webHandler", DispatcherHandler.class); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java new file mode 100644 index 0000000000..f2ba89517e --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.oauth2.server.resource.web.access.server; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; + +/** + * Translates any {@link AccessDeniedException} into an HTTP response in accordance with + * RFC 6750 Section 3: The WWW-Authenticate. + * + * So long as the class can prove that the request has a valid OAuth 2.0 {@link Authentication}, then will return an + * insufficient scope error; otherwise, + * it will simply indicate the scheme (Bearer) and any configured realm. + * + * @author Josh Cummings + * @since 5.1 + * + */ +public class BearerTokenServerAccessDeniedHandler implements ServerAccessDeniedHandler { + private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = + Arrays.asList("scope", "scp"); + + private String realmName; + + @Override + public Mono handle(ServerWebExchange exchange, AccessDeniedException denied) { + + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + return exchange.getPrincipal() + .filter(AbstractOAuth2TokenAuthenticationToken.class::isInstance) + .cast(AbstractOAuth2TokenAuthenticationToken.class) + .map(token -> errorMessageParameters(token, parameters)) + .switchIfEmpty(Mono.just(parameters)) + .flatMap(params -> respond(exchange, params)); + } + + /** + * Set the default realm name to use in the bearer token error response + * + * @param realmName + */ + public final void setRealmName(String realmName) { + this.realmName = realmName; + } + + private static Map errorMessageParameters( + AbstractOAuth2TokenAuthenticationToken token, + Map parameters) { + + String scope = getScope(token); + + parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE); + parameters.put("error_description", + String.format("The token provided has insufficient scope [%s] for this request", scope)); + parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1"); + + if (StringUtils.hasText(scope)) { + parameters.put("scope", scope); + } + + return parameters; + } + + private static Mono respond(ServerWebExchange exchange, Map parameters) { + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); + exchange.getResponse().getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + return exchange.getResponse().setComplete(); + } + + private static String getScope(AbstractOAuth2TokenAuthenticationToken token) { + + Map attributes = token.getTokenAttributes(); + + for (String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES) { + Object scopes = attributes.get(attributeName); + if (scopes instanceof String) { + return (String) scopes; + } else if (scopes instanceof Collection) { + Collection coll = (Collection) scopes; + return (String) coll.stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + } + } + + return ""; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java new file mode 100644 index 0000000000..c618da82fe --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandlerTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.oauth2.server.resource.web.access.server; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.assertj.core.util.Maps; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BearerTokenServerAccessDeniedHandlerTests { + private BearerTokenServerAccessDeniedHandler accessDeniedHandler; + + @Before + public void setUp() { + this.accessDeniedHandler = new BearerTokenServerAccessDeniedHandler(); + } + + @Test + public void handleWhenNotOAuth2AuthenticatedThenStatus403() { + + Authentication token = new TestingAuthenticationToken("user", "pass"); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer")); + } + + @Test + public void handleWhenNotOAuth2AuthenticatedAndRealmSetThenStatus403AndAuthHeaderWithRealm() { + + Authentication token = new TestingAuthenticationToken("user", "pass"); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.setRealmName("test"); + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer realm=\"test\"")); + } + + @Test + public void handleWhenTokenHasNoScopesThenInsufficientScopeError() { + + Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap()); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"")); + } + + + @Test + public void handleWhenTokenHasScopeAttributeThenInsufficientScopeErrorWithScopes() { + Map attributes = Maps.newHashMap("scope", "message:read message:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\"")); + } + + @Test + public void handleWhenTokenHasEmptyScopeAttributeThenInsufficientScopeError() { + Map attributes = Maps.newHashMap("scope", ""); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"")); + } + + @Test + public void handleWhenTokenHasScpAttributeThenInsufficientScopeErrorWithScopes() { + Map attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\"")); + } + + @Test + public void handleWhenTokenHasEmptyScpAttributeThenInsufficientScopeError() { + + Map attributes = Maps.newHashMap("scp", Collections.emptyList()); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"")); + } + + @Test + public void handleWhenTokenHasBothScopeAndScpAttributesTheInsufficientErrorBasedOnScopeAttribute() { + Map attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + attributes.put("scope", "missive:read missive:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")).isEqualTo( + Arrays.asList("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [missive:read missive:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"missive:read missive:write\"")); + } + + @Test + public void handleWhenTokenHasScopeAttributeAndRealmIsSetThenInsufficientScopeErrorWithScopesAndRealm() { + Map attributes = Maps.newHashMap("scope", "message:read message:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getPrincipal()).thenReturn(Mono.just(token)); + when(exchange.getResponse()).thenReturn(new MockServerHttpResponse()); + + this.accessDeniedHandler.setRealmName("test"); + this.accessDeniedHandler.handle(exchange, null).block(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(exchange.getResponse().getHeaders().get("WWW-Authenticate")) + .isEqualTo(Arrays.asList("Bearer realm=\"test\", " + + "error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\"")); + } + + @Test + public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { + assertThatCode(() -> this.accessDeniedHandler.setRealmName(null)) + .doesNotThrowAnyException(); + } + + static class TestingOAuth2TokenAuthenticationToken + extends AbstractOAuth2TokenAuthenticationToken { + + private Map attributes; + + protected TestingOAuth2TokenAuthenticationToken(Map attributes) { + super(new TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token("token")); + this.attributes = attributes; + } + + @Override + public Map getTokenAttributes() { + return this.attributes; + } + + static class TestingOAuth2Token extends AbstractOAuth2Token { + public TestingOAuth2Token(String tokenValue) { + super(tokenValue); + } + } + } +}