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.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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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