Skip to content

Commit 05e6240

Browse files
author
Dave Syer
committed
Support for mTLS in client
Fixes #10
1 parent 148e1d1 commit 05e6240

File tree

15 files changed

+257
-248
lines changed

15 files changed

+257
-248
lines changed

samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020

21-
import org.junit.jupiter.api.Disabled;
2221
import org.junit.jupiter.api.Nested;
2322
import org.junit.jupiter.api.Test;
2423
import org.junit.jupiter.api.condition.EnabledOnOs;
@@ -122,12 +121,13 @@ void clientChannelWithSsl(@Autowired GrpcChannelFactory channels) {
122121

123122
@Nested
124123
@SpringBootTest(properties = { "spring.grpc.server.port=0", "spring.grpc.server.ssl.client-auth=REQUIRE",
124+
"spring.grpc.server.ssl.secure=false",
125125
"spring.grpc.client.channels.test-channel.address=static://0.0.0.0:${local.grpc.port}",
126+
"spring.grpc.client.channels.test-channel.ssl.bundle=ssltest",
126127
"spring.grpc.client.channels.test-channel.negotiation-type=TLS",
127128
"spring.grpc.client.channels.test-channel.secure=false" })
128129
@ActiveProfiles("ssl")
129130
@DirtiesContext
130-
@Disabled("Requires client certificate")
131131
class ServerWithClientAuth {
132132

133133
@Test
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2024-2024 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+
package org.springframework.grpc.client;
17+
18+
import io.grpc.ChannelCredentials;
19+
import io.grpc.InsecureChannelCredentials;
20+
21+
/**
22+
* A provider for obtaining channel credentials for gRPC client.
23+
*/
24+
public interface ChannelCredentialsProvider {
25+
26+
static final ChannelCredentialsProvider INSECURE = path -> InsecureChannelCredentials.create();
27+
28+
ChannelCredentials getChannelCredentials(String path);
29+
30+
}

spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class DefaultGrpcChannelFactory implements GrpcChannelFactory, Disposable
3737

3838
private final List<GrpcChannelConfigurer> configurers = new ArrayList<>();
3939

40+
private ChannelCredentialsProvider credentials = ChannelCredentialsProvider.INSECURE;
41+
4042
private VirtualTargets targets = VirtualTargets.DEFAULT;
4143

4244
public DefaultGrpcChannelFactory() {
@@ -51,10 +53,15 @@ public void setVirtualTargets(VirtualTargets targets) {
5153
this.targets = targets;
5254
}
5355

56+
public void setCredentialsProvider(ChannelCredentialsProvider credentials) {
57+
this.credentials = credentials;
58+
}
59+
5460
@Override
5561
public ManagedChannelBuilder<?> createChannel(String authority) {
5662
ManagedChannelBuilder<?> target = builders.computeIfAbsent(authority, path -> {
57-
ManagedChannelBuilder<?> builder = newChannel(targets.getTarget(path));
63+
ManagedChannelBuilder<?> builder = newChannel(targets.getTarget(path),
64+
credentials.getChannelCredentials(path));
5865
for (GrpcChannelConfigurer configurer : configurers) {
5966
configurer.configure(path, builder);
6067
}
@@ -64,12 +71,8 @@ public ManagedChannelBuilder<?> createChannel(String authority) {
6471

6572
}
6673

67-
protected ChannelCredentials channelCredentials(String path) {
68-
return InsecureChannelCredentials.create();
69-
}
70-
71-
protected ManagedChannelBuilder<?> newChannel(String path) {
72-
return Grpc.newChannelBuilder(path, channelCredentials(path));
74+
protected ManagedChannelBuilder<?> newChannel(String path, ChannelCredentials creds) {
75+
return Grpc.newChannelBuilder(path, creds);
7376
}
7477

7578
@Override

spring-grpc-core/src/main/java/org/springframework/grpc/client/NettyGrpcChannelFactory.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,10 @@
1717

1818
import java.util.List;
1919

20-
import io.grpc.ManagedChannelBuilder;
21-
import io.grpc.netty.NettyChannelBuilder;
22-
2320
public class NettyGrpcChannelFactory extends DefaultGrpcChannelFactory {
2421

2522
public NettyGrpcChannelFactory(List<GrpcChannelConfigurer> configurers) {
2623
super(configurers);
2724
}
2825

29-
protected ManagedChannelBuilder<?> newChannel(String path) {
30-
if (path.startsWith("unix:")) {
31-
return super.newChannel(path);
32-
}
33-
return NettyChannelBuilder.forTarget(path);
34-
}
35-
3626
}

spring-grpc-core/src/main/java/org/springframework/grpc/client/ShadedNettyGrpcChannelFactory.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,10 @@
1717

1818
import java.util.List;
1919

20-
import io.grpc.ManagedChannelBuilder;
21-
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
22-
2320
public class ShadedNettyGrpcChannelFactory extends DefaultGrpcChannelFactory {
2421

2522
public ShadedNettyGrpcChannelFactory(List<GrpcChannelConfigurer> configurers) {
2623
super(configurers);
2724
}
2825

29-
protected ManagedChannelBuilder<?> newChannel(String path) {
30-
if (path.startsWith("unix:")) {
31-
return super.newChannel(path);
32-
}
33-
return NettyChannelBuilder.forTarget(path);
34-
}
35-
3626
}

spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
|spring.grpc.server.port | `+++9090+++` | Server port to listen on. When the value is 0, a random available port is selected. The default is 9090.
3131
|spring.grpc.server.shutdown-grace-period | `+++30s+++` | Maximum time to wait for the server to gracefully shutdown. When the value is negative, the server waits forever. When the value is 0, the server will force shutdown immediately. The default is 30 seconds.
3232
|spring.grpc.server.ssl.bundle | | SSL bundle name.
33+
|spring.grpc.server.ssl.client-auth | | Client authentication mode.
3334
|spring.grpc.server.ssl.enabled | | Whether to enable SSL support. Enabled automatically if "bundle" is provided unless specified otherwise.
35+
|spring.grpc.server.ssl.secure | `+++true+++` | Flag to indicate that client authentication is secure (i.e. certificates are checked). Do not set this to false in production.
3436

3537
|===

spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java

Lines changed: 10 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -15,133 +15,39 @@
1515
*/
1616
package org.springframework.grpc.autoconfigure.client;
1717

18-
import java.util.List;
19-
20-
import javax.net.ssl.SSLException;
21-
2218
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2319
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
20+
import org.springframework.boot.ssl.SslBundles;
2421
import org.springframework.context.annotation.Bean;
2522
import org.springframework.context.annotation.Configuration;
26-
import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.NamedChannel;
27-
import org.springframework.grpc.client.DefaultGrpcChannelFactory;
28-
import org.springframework.grpc.client.GrpcChannelConfigurer;
29-
import org.springframework.grpc.client.GrpcChannelFactory;
30-
import org.springframework.grpc.client.NegotiationType;
31-
import org.springframework.grpc.client.NettyGrpcChannelFactory;
32-
import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory;
33-
import org.springframework.grpc.client.VirtualTargets;
23+
import org.springframework.grpc.client.ChannelCredentialsProvider;
3424

35-
import io.grpc.netty.GrpcSslContexts;
3625
import io.grpc.netty.NettyChannelBuilder;
37-
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
3826

3927
public class GrpcChannelFactoryConfigurations {
4028

4129
@Configuration(proxyBeanMethods = false)
4230
@ConditionalOnClass(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)
43-
@ConditionalOnMissingBean(GrpcChannelFactory.class)
31+
@ConditionalOnMissingBean(ChannelCredentialsProvider.class)
4432
public static class ShadedNettyChannelFactoryConfiguration {
4533

4634
@Bean
47-
public DefaultGrpcChannelFactory defaultGrpcChannelFactory(final List<GrpcChannelConfigurer> configurers,
48-
GrpcClientProperties channels) {
49-
DefaultGrpcChannelFactory factory = new ShadedNettyGrpcChannelFactory(configurers);
50-
factory.setVirtualTargets(new NamedChannelVirtualTargets(channels));
51-
return factory;
52-
}
53-
54-
@Bean
55-
public GrpcChannelConfigurer secureChannelConfigurer(GrpcClientProperties channels) {
56-
57-
return (authority, input) -> {
58-
NamedChannel channel = channels.getChannel(authority);
59-
if (!authority.startsWith("unix:")
60-
&& input instanceof io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder builder) {
61-
builder.negotiationType(of(channel.getNegotiationType()));
62-
try {
63-
if (!channel.isSecure()) {
64-
builder.sslContext(io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts.forClient()
65-
.trustManager(
66-
io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE)
67-
.build());
68-
}
69-
}
70-
catch (SSLException e) {
71-
throw new IllegalStateException("Failed to create SSL context", e);
72-
}
73-
}
74-
};
75-
76-
}
77-
78-
private static io.grpc.netty.shaded.io.grpc.netty.NegotiationType of(final NegotiationType negotiationType) {
79-
return switch (negotiationType) {
80-
case PLAINTEXT -> io.grpc.netty.shaded.io.grpc.netty.NegotiationType.PLAINTEXT;
81-
case PLAINTEXT_UPGRADE -> io.grpc.netty.shaded.io.grpc.netty.NegotiationType.PLAINTEXT_UPGRADE;
82-
case TLS -> io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS;
83-
};
35+
public ChannelCredentialsProvider channelCredentialsProvider(GrpcClientProperties channels,
36+
SslBundles bundles) {
37+
return new ShadedNettyChannelCredentialsProvider(bundles, channels);
8438
}
8539

8640
}
8741

8842
@Configuration(proxyBeanMethods = false)
8943
@ConditionalOnClass(NettyChannelBuilder.class)
90-
@ConditionalOnMissingBean(GrpcChannelFactory.class)
44+
@ConditionalOnMissingBean(ChannelCredentialsProvider.class)
9145
public static class NettyChannelFactoryConfiguration {
9246

9347
@Bean
94-
public DefaultGrpcChannelFactory defaultGrpcChannelFactory(final List<GrpcChannelConfigurer> configurers,
95-
GrpcClientProperties channels) {
96-
DefaultGrpcChannelFactory factory = new NettyGrpcChannelFactory(configurers);
97-
factory.setVirtualTargets(new NamedChannelVirtualTargets(channels));
98-
return factory;
99-
}
100-
101-
@Bean
102-
public GrpcChannelConfigurer secureChannelConfigurer(GrpcClientProperties channels) {
103-
104-
return (authority, input) -> {
105-
NamedChannel channel = channels.getChannel(authority);
106-
if (!authority.startsWith("unix:") && input instanceof NettyChannelBuilder builder) {
107-
builder.negotiationType(of(channel.getNegotiationType()));
108-
try {
109-
if (!channel.isSecure()) {
110-
builder.sslContext(GrpcSslContexts.forClient()
111-
.trustManager(InsecureTrustManagerFactory.INSTANCE)
112-
.build());
113-
}
114-
}
115-
catch (SSLException e) {
116-
throw new IllegalStateException("Failed to create SSL context", e);
117-
}
118-
}
119-
};
120-
121-
}
122-
123-
private static io.grpc.netty.NegotiationType of(final NegotiationType negotiationType) {
124-
return switch (negotiationType) {
125-
case PLAINTEXT -> io.grpc.netty.NegotiationType.PLAINTEXT;
126-
case PLAINTEXT_UPGRADE -> io.grpc.netty.NegotiationType.PLAINTEXT_UPGRADE;
127-
case TLS -> io.grpc.netty.NegotiationType.TLS;
128-
};
129-
}
130-
131-
}
132-
133-
static class NamedChannelVirtualTargets implements VirtualTargets {
134-
135-
private final GrpcClientProperties channels;
136-
137-
NamedChannelVirtualTargets(GrpcClientProperties channels) {
138-
this.channels = channels;
139-
}
140-
141-
@Override
142-
public String getTarget(String authority) {
143-
NamedChannel channel = this.channels.getChannel(authority);
144-
return channels.getTarget(channel.getAddress());
48+
public ChannelCredentialsProvider channelCredentialsProvider(GrpcClientProperties channels,
49+
SslBundles bundles) {
50+
return new NettyChannelCredentialsProvider(bundles, channels);
14551
}
14652

14753
}

spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@
1515
*/
1616
package org.springframework.grpc.autoconfigure.client;
1717

18+
import java.util.List;
1819
import java.util.concurrent.TimeUnit;
1920

20-
import io.grpc.CompressorRegistry;
21-
import io.grpc.DecompressorRegistry;
2221
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2323
import org.springframework.boot.context.properties.EnableConfigurationProperties;
24-
import org.springframework.boot.ssl.SslBundle;
2524
import org.springframework.boot.ssl.SslBundles;
2625
import org.springframework.context.annotation.Bean;
2726
import org.springframework.context.annotation.Configuration;
2827
import org.springframework.context.annotation.Import;
2928
import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.NamedChannel;
3029
import org.springframework.grpc.autoconfigure.common.codec.GrpcCodecConfiguration;
30+
import org.springframework.grpc.client.ChannelCredentialsProvider;
31+
import org.springframework.grpc.client.DefaultGrpcChannelFactory;
3132
import org.springframework.grpc.client.GrpcChannelConfigurer;
33+
import org.springframework.grpc.client.GrpcChannelFactory;
34+
import org.springframework.grpc.client.VirtualTargets;
35+
36+
import io.grpc.CompressorRegistry;
37+
import io.grpc.DecompressorRegistry;
3238

3339
@Configuration(proxyBeanMethods = false)
3440
@EnableConfigurationProperties(GrpcClientProperties.class)
@@ -37,26 +43,21 @@
3743
public class GrpcClientAutoConfiguration {
3844

3945
@Bean
40-
public GrpcChannelConfigurer sslGrpcChannelConfigurer(GrpcClientProperties channels, SslBundles bundles) {
46+
@ConditionalOnMissingBean(GrpcChannelFactory.class)
47+
public DefaultGrpcChannelFactory defaultGrpcChannelFactory(final List<GrpcChannelConfigurer> configurers,
48+
ChannelCredentialsProvider credentials, GrpcClientProperties channels, SslBundles bundles) {
49+
DefaultGrpcChannelFactory factory = new DefaultGrpcChannelFactory(configurers);
50+
factory.setCredentialsProvider(credentials);
51+
factory.setVirtualTargets(new NamedChannelVirtualTargets(channels));
52+
return factory;
53+
}
54+
55+
@Bean
56+
public GrpcChannelConfigurer sslGrpcChannelConfigurer(GrpcClientProperties channels) {
4157
return (authority, builder) -> {
4258
for (String name : channels.getChannels().keySet()) {
4359
if (authority.equals(name)) {
4460
NamedChannel channel = channels.getChannels().get(name);
45-
if (channel.getSsl().isEnabled() && channel.getSsl().getBundle() != null) {
46-
SslBundle bundle = bundles.getBundle(channel.getSsl().getBundle());
47-
if (NettyChannelFactoryHelper.isAvailable()) {
48-
NettyChannelFactoryHelper.sslContext(builder, bundle);
49-
}
50-
else if (ShadedNettyChannelFactoryHelper.isAvailable()) {
51-
ShadedNettyChannelFactoryHelper.sslContext(builder, bundle);
52-
}
53-
else {
54-
throw new IllegalStateException("Netty is not available");
55-
}
56-
}
57-
else {
58-
// builder.usePlaintext();
59-
}
6061
if (channel.getUserAgent() != null) {
6162
builder.userAgent(channel.getUserAgent());
6263
}
@@ -96,4 +97,20 @@ GrpcChannelConfigurer decompressionClientConfigurer(DecompressorRegistry registr
9697
return (name, builder) -> builder.decompressorRegistry(registry);
9798
}
9899

100+
static class NamedChannelVirtualTargets implements VirtualTargets {
101+
102+
private final GrpcClientProperties channels;
103+
104+
NamedChannelVirtualTargets(GrpcClientProperties channels) {
105+
this.channels = channels;
106+
}
107+
108+
@Override
109+
public String getTarget(String authority) {
110+
NamedChannel channel = this.channels.getChannel(authority);
111+
return channels.getTarget(channel.getAddress());
112+
}
113+
114+
}
115+
99116
}

0 commit comments

Comments
 (0)