Reactive Resource Server insufficient_scope

This introduces an implementation of ServerAccessDeniedHandler that is
compliant with the OAuth 2.0 spec for insufficent_scope errors.

Fixes: gh-5705
This commit is contained in:
Josh Cummings 2018-08-24 13:28:20 -06:00 committed by Rob Winch
parent 1c74706232
commit 8510e9a285
4 changed files with 465 additions and 16 deletions

View File

@ -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.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; 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.BearerTokenServerAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint; 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.DelegatingReactiveAuthorizationManager;
import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; 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.NoOpServerSecurityContextRepository;
import org.springframework.security.web.server.context.ReactorContextWebFilter; import org.springframework.security.web.server.context.ReactorContextWebFilter;
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
@ -230,6 +232,9 @@ public class ServerHttpSecurity {
private ServerAccessDeniedHandler accessDeniedHandler; private ServerAccessDeniedHandler accessDeniedHandler;
private List<ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry>
defaultAccessDeniedHandlers = new ArrayList<>();
private List<WebFilter> webFilters = new ArrayList<>(); private List<WebFilter> webFilters = new ArrayList<>();
private ApplicationContext context; private ApplicationContext context;
@ -687,6 +692,9 @@ public class ServerHttpSecurity {
* Configures OAuth2 Resource Server Support * Configures OAuth2 Resource Server Support
*/ */
public class OAuth2ResourceServerSpec { public class OAuth2ResourceServerSpec {
private BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
private BearerTokenServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
private JwtSpec jwt; private JwtSpec jwt;
public JwtSpec jwt() { public JwtSpec jwt() {
@ -752,9 +760,10 @@ public class ServerHttpSecurity {
new ServerBearerTokenAuthenticationConverter(); new ServerBearerTokenAuthenticationConverter();
this.bearerTokenServerWebExchangeMatcher.setBearerTokenConverter(bearerTokenConverter); this.bearerTokenServerWebExchangeMatcher.setBearerTokenConverter(bearerTokenConverter);
registerDefaultAccessDeniedHandler(http);
registerDefaultAuthenticationEntryPoint(http);
registerDefaultCsrfOverride(http); registerDefaultCsrfOverride(http);
BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
ReactiveJwtDecoder jwtDecoder = getJwtDecoder(); ReactiveJwtDecoder jwtDecoder = getJwtDecoder();
JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager( JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager(
jwtDecoder); jwtDecoder);
@ -763,9 +772,6 @@ public class ServerHttpSecurity {
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint)); oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
http http
.exceptionHandling()
.authenticationEntryPoint(entryPoint)
.and()
.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); .addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
} }
@ -776,6 +782,28 @@ public class ServerHttpSecurity {
return this.jwtDecoder; 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) { private void registerDefaultCsrfOverride(ServerHttpSecurity http) {
if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) { if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) {
http http
@ -1033,8 +1061,10 @@ public class ServerHttpSecurity {
exceptionTranslationWebFilter.setAuthenticationEntryPoint( exceptionTranslationWebFilter.setAuthenticationEntryPoint(
authenticationEntryPoint); authenticationEntryPoint);
} }
if (this.accessDeniedHandler != null) { ServerAccessDeniedHandler accessDeniedHandler = getAccessDeniedHandler();
exceptionTranslationWebFilter.setAccessDeniedHandler(this.accessDeniedHandler); if (accessDeniedHandler != null) {
exceptionTranslationWebFilter.setAccessDeniedHandler(
accessDeniedHandler);
} }
this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION); this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION);
this.authorizeExchange.configure(this); this.authorizeExchange.configure(this);
@ -1077,6 +1107,20 @@ public class ServerHttpSecurity {
return result; 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. * Creates a new instance.
* @return the new {@link ServerHttpSecurity} instance * @return the new {@link ServerHttpSecurity} instance

View File

@ -171,6 +171,17 @@ public class OAuth2ResourceServerSpecTests {
.expectStatus().isOk(); .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 @Test
public void postWhenMissingTokenThenReturnsForbidden() { public void postWhenMissingTokenThenReturnsForbidden() {
this.spring.register(PublicKeyConfig.class, RootController.class).autowire(); this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
@ -248,22 +259,17 @@ public class OAuth2ResourceServerSpecTests {
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception {
// @formatter:off // @formatter:off
http http
.authorizeExchange()
.anyExchange().hasAuthority("SCOPE_message:read")
.and()
.oauth2ResourceServer() .oauth2ResourceServer()
.jwt() .jwt()
.publicKey(this.publicKey()); .publicKey(publicKey());
// @formatter:on // @formatter:on
return http.build(); 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 @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 @RestController
static class RootController { static class RootController {
@GetMapping @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() { private GenericWebApplicationContext autowireWebServerGenericWebApplicationContext() {
GenericWebApplicationContext context = new GenericWebApplicationContext(); GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean("webHandler", DispatcherHandler.class); context.registerBean("webHandler", DispatcherHandler.class);

View File

@ -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
* <a href="https://tools.ietf.org/html/rfc6750#section-3" target="_blank">RFC 6750 Section 3: The WWW-Authenticate</a>.
*
* So long as the class can prove that the request has a valid OAuth 2.0 {@link Authentication}, then will return an
* <a href="https://tools.ietf.org/html/rfc6750#section-3.1" target="_blank">insufficient scope error</a>; 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<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
Arrays.asList("scope", "scp");
private String realmName;
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
Map<String, String> 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<String, String> errorMessageParameters(
AbstractOAuth2TokenAuthenticationToken token,
Map<String, String> 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<Void> respond(ServerWebExchange exchange, Map<String, String> 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<String, Object> 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<String, String> parameters) {
String wwwAuthenticate = "Bearer";
if (!parameters.isEmpty()) {
wwwAuthenticate += parameters.entrySet().stream()
.map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"")
.collect(Collectors.joining(", ", " ", ""));
}
return wwwAuthenticate;
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token> {
private Map<String, Object> attributes;
protected TestingOAuth2TokenAuthenticationToken(Map<String, Object> attributes) {
super(new TestingOAuth2TokenAuthenticationToken.TestingOAuth2Token("token"));
this.attributes = attributes;
}
@Override
public Map<String, Object> getTokenAttributes() {
return this.attributes;
}
static class TestingOAuth2Token extends AbstractOAuth2Token {
public TestingOAuth2Token(String tokenValue) {
super(tokenValue);
}
}
}
}