From 33ba292fede830b15bf16fdb1691f3968a71eb2e Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 27 Sep 2019 10:47:27 -0600 Subject: [PATCH] Resource Server w/ SecurityReactorContextSubscriber Fixes gh-7423 --- .../configuration/OAuth2ImportSelector.java | 21 +-- .../OAuth2ResourceServerConfiguration.java | 144 ------------------ ...textConfigurationResourceServerTests.java} | 4 +- .../ServletBearerExchangeFilterFunction.java | 17 ++- ...vletBearerExchangeFilterFunctionTests.java | 14 +- 5 files changed, 39 insertions(+), 161 deletions(-) delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfiguration.java rename config/src/test/java/org/springframework/security/config/annotation/web/configuration/{OAuth2ResourceServerConfigurationTests.java => SecurityReactorContextConfigurationResourceServerTests.java} (96%) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector.java index 3b5b925fe4..a999d5c7f8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector.java @@ -15,20 +15,21 @@ */ package org.springframework.security.config.annotation.web.configuration; +import java.util.LinkedHashSet; +import java.util.Set; + import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.ClassUtils; -import java.util.ArrayList; -import java.util.List; - /** * Used by {@link EnableWebSecurity} to conditionally import: * * * * @author Joe Grandja @@ -36,13 +37,12 @@ import java.util.List; * @since 5.1 * @see OAuth2ClientConfiguration * @see SecurityReactorContextConfiguration - * @see OAuth2ResourceServerConfiguration */ final class OAuth2ImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { - List imports = new ArrayList<>(); + Set imports = new LinkedHashSet<>(); boolean oauth2ClientPresent = ClassUtils.isPresent( "org.springframework.security.oauth2.client.registration.ClientRegistration", getClass().getClassLoader()); @@ -56,9 +56,10 @@ final class OAuth2ImportSelector implements ImportSelector { imports.add("org.springframework.security.config.annotation.web.configuration.SecurityReactorContextConfiguration"); } - if (ClassUtils.isPresent( - "org.springframework.security.oauth2.server.resource.BearerTokenError", getClass().getClassLoader())) { - imports.add("org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration"); + boolean oauth2ResourceServerPresent = ClassUtils.isPresent( + "org.springframework.security.oauth2.server.resource.BearerTokenError", getClass().getClassLoader()); + if (webfluxPresent && oauth2ResourceServerPresent) { + imports.add("org.springframework.security.config.annotation.web.configuration.SecurityReactorContextConfiguration"); } return imports.toArray(new String[0]); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfiguration.java deleted file mode 100644 index 34ec7efeda..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfiguration.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2002-2019 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 - * - * https://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.config.annotation.web.configuration; - -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Hooks; -import reactor.core.publisher.Operators; -import reactor.util.context.Context; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.ClassUtils; - -/** - * {@link Configuration} for OAuth 2.0 Resource Server support. - * - *

- * This {@code Configuration} is conditionally imported by {@link OAuth2ImportSelector} - * when the {@code spring-security-oauth2-resource-server} module is present on the classpath. - * - * @author Josh Cummings - * @since 5.2 - * @see OAuth2ImportSelector - */ -@Import(OAuth2ResourceServerConfiguration.OAuth2ClientWebFluxImportSelector.class) -final class OAuth2ResourceServerConfiguration { - - static class OAuth2ClientWebFluxImportSelector implements ImportSelector { - - @Override - public String[] selectImports(AnnotationMetadata importingClassMetadata) { - boolean webfluxPresent = ClassUtils.isPresent( - "org.springframework.web.reactive.function.client.WebClient", getClass().getClassLoader()); - - return webfluxPresent ? - new String[] { "org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration.OAuth2ResourceServerWebFluxSecurityConfiguration" } : - new String[] {}; - } - } - - @Configuration(proxyBeanMethods = false) - static class OAuth2ResourceServerWebFluxSecurityConfiguration { - @Bean - BearerRequestContextSubscriberRegistrar bearerRequestContextSubscriberRegistrar() { - return new BearerRequestContextSubscriberRegistrar(); - } - - /** - * Registers a {@link CoreSubscriber} that provides the current {@link Authentication} - * to the correct {@link Context}. - * - * This is published as a {@code @Bean} automatically, so long as `spring-security-oauth2-resource-server` - * and `spring-webflux` are on the classpath. - */ - static class BearerRequestContextSubscriberRegistrar - implements InitializingBean, DisposableBean { - - private static final String REQUEST_CONTEXT_OPERATOR_KEY = BearerRequestContextSubscriber.class.getName(); - - @Override - public void afterPropertiesSet() throws Exception { - Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY, - Operators.liftPublisher((s, sub) -> createRequestContextSubscriber(sub))); - } - - @Override - public void destroy() throws Exception { - Hooks.resetOnLastOperator(REQUEST_CONTEXT_OPERATOR_KEY); - } - - private CoreSubscriber createRequestContextSubscriber(CoreSubscriber delegate) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return new BearerRequestContextSubscriber<>(delegate, authentication); - } - - static class BearerRequestContextSubscriber implements CoreSubscriber { - private CoreSubscriber delegate; - private final Context context; - - private BearerRequestContextSubscriber(CoreSubscriber delegate, - Authentication authentication) { - - this.delegate = delegate; - Context parentContext = this.delegate.currentContext(); - Context context; - if (authentication == null || parentContext.hasKey(Authentication.class)) { - context = parentContext; - } else { - context = parentContext.put(Authentication.class, authentication); - } - - this.context = context; - } - - @Override - public Context currentContext() { - return this.context; - } - - @Override - public void onSubscribe(Subscription s) { - this.delegate.onSubscribe(s); - } - - @Override - public void onNext(T t) { - this.delegate.onNext(t); - } - - @Override - public void onError(Throwable t) { - this.delegate.onError(t); - } - - @Override - public void onComplete() { - this.delegate.onComplete(); - } - } - } - } -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationResourceServerTests.java similarity index 96% rename from config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfigurationTests.java rename to config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationResourceServerTests.java index e93155c41c..23e84c88b9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ResourceServerConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationResourceServerTests.java @@ -44,11 +44,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Tests for {@link OAuth2ResourceServerConfiguration}. + * Tests for applications of {@link SecurityReactorContextConfiguration} in resource servers. * * @author Josh Cummings */ -public class OAuth2ResourceServerConfigurationTests { +public class SecurityReactorContextConfigurationResourceServerTests { @Rule public final SpringTestRule spring = new SpringTestRule(); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java index 892ed53423..f59af70f02 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.server.resource.web.reactive.function.client; +import java.util.Map; + import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -56,6 +58,9 @@ import org.springframework.web.reactive.function.client.ExchangeFunction; public final class ServletBearerExchangeFilterFunction implements ExchangeFilterFunction { + static final String SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = + "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES"; + /** * {@inheritDoc} */ @@ -76,8 +81,16 @@ public final class ServletBearerExchangeFilterFunction } private Mono currentAuthentication(Context ctx) { - Authentication authentication = ctx.getOrDefault(Authentication.class, null); - return Mono.justOrEmpty(authentication); + return Mono.justOrEmpty(getAttribute(ctx, Authentication.class)); + } + + private T getAttribute(Context ctx, Class clazz) { + // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key + if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) { + return null; + } + Map, T> attributes = ctx.get(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY); + return attributes.get(clazz); } private ClientRequest bearer(ClientRequest request, AbstractOAuth2Token token) { diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunctionTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunctionTests.java index e960a6e85f..e5fd79b339 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunctionTests.java @@ -20,6 +20,7 @@ import java.net.URI; import java.time.Duration; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -37,6 +38,7 @@ import org.springframework.web.reactive.function.client.ClientRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.HttpMethod.GET; +import static org.springframework.security.oauth2.server.resource.web.reactive.function.client.ServletBearerExchangeFilterFunction.SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY; /** * Tests for {@link ServletBearerExchangeFilterFunction} @@ -80,7 +82,7 @@ public class ServletBearerExchangeFilterFunctionTests { .build(); this.function.filter(request, this.exchange) - .subscriberContext(Context.of(Authentication.class, token)) + .subscriberContext(context(token)) .block(); assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)) @@ -93,7 +95,7 @@ public class ServletBearerExchangeFilterFunctionTests { .build(); this.function.filter(request, this.exchange) - .subscriberContext(Context.of(Authentication.class, this.authentication)) + .subscriberContext(context(this.authentication)) .block(); assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)) @@ -107,10 +109,16 @@ public class ServletBearerExchangeFilterFunctionTests { .build(); this.function.filter(request, this.exchange) - .subscriberContext(Context.of(Authentication.class, this.authentication)) + .subscriberContext(context(this.authentication)) .block(); HttpHeaders headers = this.exchange.getRequest().headers(); assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue()); } + + private Context context(Authentication authentication) { + Map, Object> contextAttributes = new HashMap<>(); + contextAttributes.put(Authentication.class, authentication); + return Context.of(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY, contextAttributes); + } }