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:
parent
1c74706232
commit
8510e9a285
|
@ -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<ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry>
|
||||
defaultAccessDeniedHandlers = new ArrayList<>();
|
||||
|
||||
private List<WebFilter> 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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue