Skip to content

Commit 6dbcd0e

Browse files
committed
Auto-configure security for Spring GraphQL
This commit configures security features for Spring GraphQL. In the case of both MVC and WebFlux, this contributes `DataFetcherExceptionResolver` instances to resolve security exceptions and expose them as proper errors in the GraphQL response. For MVC only, this also configures a `SecurityContextThreadLocalAccessor`. This component ensures that the security context is propagated between `ThreadLocal` and the Reactor asynchronous execution. See gh-29140
1 parent 9954b4c commit 6dbcd0e

File tree

6 files changed

+479
-0
lines changed

6 files changed

+479
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.graphql.security;
18+
19+
import graphql.GraphQL;
20+
21+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
22+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
27+
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
31+
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
32+
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
33+
34+
/**
35+
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for
36+
* Spring GraphQL with WebFlux.
37+
*
38+
* @author Brian Clozel
39+
* @since 2.7.0
40+
*/
41+
@Configuration(proxyBeanMethods = false)
42+
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
43+
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebFluxSecurity.class })
44+
@ConditionalOnBean(GraphQlHttpHandler.class)
45+
@AutoConfigureAfter(GraphQlWebFluxAutoConfiguration.class)
46+
public class GraphQlWebFluxSecurityAutoConfiguration {
47+
48+
@Bean
49+
@ConditionalOnMissingBean
50+
public ReactiveSecurityDataFetcherExceptionResolver reactiveSecurityDataFetcherExceptionResolver() {
51+
return new ReactiveSecurityDataFetcherExceptionResolver();
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.graphql.security;
18+
19+
import graphql.GraphQL;
20+
21+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
22+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
27+
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.graphql.security.SecurityContextThreadLocalAccessor;
31+
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
32+
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
33+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
34+
35+
/**
36+
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for
37+
* Spring GraphQL with MVC.
38+
*
39+
* @author Brian Clozel
40+
* @since 2.7.0
41+
*/
42+
@Configuration(proxyBeanMethods = false)
43+
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
44+
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebSecurity.class })
45+
@ConditionalOnBean(GraphQlHttpHandler.class)
46+
@AutoConfigureAfter(GraphQlWebMvcAutoConfiguration.class)
47+
public class GraphQlWebMvcSecurityAutoConfiguration {
48+
49+
@Bean
50+
@ConditionalOnMissingBean
51+
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
52+
return new SecurityDataFetcherExceptionResolver();
53+
}
54+
55+
@Bean
56+
@ConditionalOnMissingBean
57+
public SecurityContextThreadLocalAccessor securityContextThreadLocalAccessor() {
58+
return new SecurityContextThreadLocalAccessor();
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Auto-configuration classes for Security support in Spring GraphQL.
19+
*/
20+
package org.springframework.boot.autoconfigure.graphql.security;

spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ org.springframework.boot.autoconfigure.graphql.data.GraphQlQueryByExampleAutoCon
7575
org.springframework.boot.autoconfigure.graphql.data.GraphQlQuerydslAutoConfiguration,\
7676
org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration,\
7777
org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration,\
78+
org.springframework.boot.autoconfigure.graphql.security.GraphQlWebFluxSecurityAutoConfiguration,\
79+
org.springframework.boot.autoconfigure.graphql.security.GraphQlWebMvcSecurityAutoConfiguration,\
7880
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
7981
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
8082
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2020-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.graphql.security;
18+
19+
import java.util.Collections;
20+
import java.util.function.Consumer;
21+
22+
import graphql.schema.idl.TypeRuntimeWiring;
23+
import org.junit.jupiter.api.Test;
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.boot.autoconfigure.AutoConfigurations;
27+
import org.springframework.boot.autoconfigure.graphql.Book;
28+
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
29+
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
30+
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
31+
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
32+
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
33+
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
34+
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
35+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
36+
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
37+
import org.springframework.context.annotation.Bean;
38+
import org.springframework.context.annotation.Configuration;
39+
import org.springframework.graphql.execution.ErrorType;
40+
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
41+
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
42+
import org.springframework.http.MediaType;
43+
import org.springframework.lang.Nullable;
44+
import org.springframework.security.access.prepost.PreAuthorize;
45+
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
46+
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
47+
import org.springframework.security.config.web.server.ServerHttpSecurity;
48+
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
49+
import org.springframework.security.core.userdetails.User;
50+
import org.springframework.security.core.userdetails.UserDetails;
51+
import org.springframework.security.web.server.SecurityWebFilterChain;
52+
import org.springframework.test.web.reactive.server.WebTestClient;
53+
54+
import static org.assertj.core.api.Assertions.assertThat;
55+
import static org.springframework.security.config.Customizer.withDefaults;
56+
57+
/**
58+
* Tests for {@link GraphQlWebFluxSecurityAutoConfiguration}.
59+
*
60+
* @author Brian Clozel
61+
*/
62+
class GraphQlWebFluxSecurityAutoConfigurationTests {
63+
64+
private static final String BASE_URL = "https://spring.example.org/graphql";
65+
66+
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
67+
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
68+
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
69+
GraphQlWebFluxAutoConfiguration.class, GraphQlWebFluxSecurityAutoConfiguration.class,
70+
ReactiveSecurityAutoConfiguration.class))
71+
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
72+
.withPropertyValues("spring.main.web-application-type=reactive");
73+
74+
@Test
75+
void contributesExceptionResolver() {
76+
this.contextRunner.run(
77+
(context) -> assertThat(context).hasSingleBean(ReactiveSecurityDataFetcherExceptionResolver.class));
78+
}
79+
80+
@Test
81+
void anonymousUserShouldBeUnauthorized() {
82+
testWithWebClient((client) -> {
83+
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
84+
client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
85+
.expectBody().jsonPath("data.bookById.name").doesNotExist()
86+
.jsonPath("errors[0].extensions.classification").isEqualTo(ErrorType.UNAUTHORIZED.toString());
87+
});
88+
}
89+
90+
@Test
91+
void authenticatedUserShouldGetData() {
92+
testWithWebClient((client) -> {
93+
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
94+
client.post().uri("").headers((headers) -> headers.setBasicAuth("rob", "rob"))
95+
.bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk().expectBody()
96+
.jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners")
97+
.jsonPath("errors[0].extensions.classification").doesNotExist();
98+
});
99+
}
100+
101+
private void testWithWebClient(Consumer<WebTestClient> consumer) {
102+
this.contextRunner.run((context) -> {
103+
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()
104+
.defaultHeaders((headers) -> {
105+
headers.setContentType(MediaType.APPLICATION_JSON);
106+
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
107+
}).baseUrl(BASE_URL).build();
108+
consumer.accept(client);
109+
});
110+
}
111+
112+
@Configuration(proxyBeanMethods = false)
113+
static class DataFetchersConfiguration {
114+
115+
@Bean
116+
RuntimeWiringConfigurer bookDataFetcher(BookService bookService) {
117+
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
118+
(env) -> bookService.getBookdById(env.getArgument("id"))));
119+
}
120+
121+
@Bean
122+
BookService bookService() {
123+
return new BookService();
124+
}
125+
126+
}
127+
128+
static class BookService {
129+
130+
@PreAuthorize("hasRole('USER')")
131+
@Nullable
132+
Mono<Book> getBookdById(String id) {
133+
return Mono.justOrEmpty(GraphQlTestDataFetchers.getBookById(id));
134+
}
135+
136+
}
137+
138+
@Configuration(proxyBeanMethods = false)
139+
@EnableWebFluxSecurity
140+
@EnableReactiveMethodSecurity
141+
static class SecurityConfig {
142+
143+
@Bean
144+
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
145+
return http.csrf((spec) -> spec.disable())
146+
// Demonstrate that method security works
147+
// Best practice to use both for defense in depth
148+
.authorizeExchange((requests) -> requests.anyExchange().permitAll()).httpBasic(withDefaults())
149+
.build();
150+
}
151+
152+
@Bean
153+
@SuppressWarnings("deprecation")
154+
MapReactiveUserDetailsService userDetailsService() {
155+
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
156+
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
157+
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
158+
return new MapReactiveUserDetailsService(rob, admin);
159+
}
160+
161+
}
162+
163+
}

0 commit comments

Comments
 (0)