Skip to content

Commit add3d87

Browse files
Support Couchbase authentication using client certificates
Closes gh-41520
1 parent d56dd74 commit add3d87

File tree

5 files changed

+250
-12
lines changed

5 files changed

+250
-12
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@
1616

1717
package org.springframework.boot.autoconfigure.couchbase;
1818

19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.security.GeneralSecurityException;
22+
import java.security.KeyStore;
23+
1924
import javax.net.ssl.TrustManagerFactory;
2025

26+
import com.couchbase.client.core.env.Authenticator;
27+
import com.couchbase.client.core.env.CertificateAuthenticator;
28+
import com.couchbase.client.core.env.PasswordAuthenticator;
2129
import com.couchbase.client.java.Cluster;
2230
import com.couchbase.client.java.ClusterOptions;
2331
import com.couchbase.client.java.codec.JacksonJsonSerializer;
@@ -36,16 +44,22 @@
3644
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3745
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
3846
import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration.CouchbaseCondition;
47+
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Authentication.Jks;
48+
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Authentication.Pem;
3949
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Ssl;
4050
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Timeouts;
4151
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
4252
import org.springframework.boot.context.properties.EnableConfigurationProperties;
53+
import org.springframework.boot.io.ApplicationResourceLoader;
4354
import org.springframework.boot.ssl.SslBundle;
4455
import org.springframework.boot.ssl.SslBundles;
56+
import org.springframework.boot.ssl.pem.PemSslStore;
57+
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
4558
import org.springframework.context.annotation.Bean;
4659
import org.springframework.context.annotation.Conditional;
4760
import org.springframework.context.annotation.Configuration;
4861
import org.springframework.core.Ordered;
62+
import org.springframework.core.io.Resource;
4963
import org.springframework.util.Assert;
5064
import org.springframework.util.StringUtils;
5165

@@ -58,6 +72,7 @@
5872
* @author Moritz Halbritter
5973
* @author Andy Wilkinson
6074
* @author Phillip Webb
75+
* @author Scott Frederick
6176
* @since 1.4.0
6277
*/
6378
@AutoConfiguration(after = JacksonAutoConfiguration.class)
@@ -80,25 +95,51 @@ PropertiesCouchbaseConnectionDetails couchbaseConnectionDetails() {
8095

8196
@Bean
8297
@ConditionalOnMissingBean
83-
public ClusterEnvironment couchbaseClusterEnvironment(CouchbaseConnectionDetails connectionDetails,
98+
public ClusterEnvironment couchbaseClusterEnvironment(
8499
ObjectProvider<ClusterEnvironmentBuilderCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {
85-
Builder builder = initializeEnvironmentBuilder(connectionDetails, sslBundles.getIfAvailable());
100+
Builder builder = initializeEnvironmentBuilder(sslBundles.getIfAvailable());
86101
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
87102
return builder.build();
88103
}
89104

105+
@Bean
106+
@ConditionalOnMissingBean
107+
public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectionDetails) throws IOException {
108+
if (connectionDetails.getUsername() != null && connectionDetails.getPassword() != null) {
109+
return PasswordAuthenticator.create(connectionDetails.getUsername(), connectionDetails.getPassword());
110+
}
111+
Pem pem = this.properties.getAuthentication().getPem();
112+
if (pem.getCertificates() != null) {
113+
PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey());
114+
PemSslStore store = PemSslStore.load(details);
115+
return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(),
116+
store.certificates());
117+
}
118+
Jks jks = this.properties.getAuthentication().getJks();
119+
if (jks.getLocation() != null) {
120+
Resource resource = new ApplicationResourceLoader().getResource(jks.getLocation());
121+
String keystorePassword = jks.getPassword();
122+
try (InputStream inputStream = resource.getInputStream()) {
123+
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
124+
store.load(inputStream, (keystorePassword != null) ? keystorePassword.toCharArray() : null);
125+
return CertificateAuthenticator.fromKeyStore(store, keystorePassword);
126+
}
127+
catch (GeneralSecurityException ex) {
128+
throw new IllegalStateException("Error reading Couchbase certificate store", ex);
129+
}
130+
}
131+
throw new IllegalStateException("Couchbase authentication requires username and password, or certificates");
132+
}
133+
90134
@Bean(destroyMethod = "disconnect")
91135
@ConditionalOnMissingBean
92-
public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment,
136+
public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment, Authenticator authenticator,
93137
CouchbaseConnectionDetails connectionDetails) {
94-
ClusterOptions options = ClusterOptions
95-
.clusterOptions(connectionDetails.getUsername(), connectionDetails.getPassword())
96-
.environment(couchbaseClusterEnvironment);
138+
ClusterOptions options = ClusterOptions.clusterOptions(authenticator).environment(couchbaseClusterEnvironment);
97139
return Cluster.connect(connectionDetails.getConnectionString(), options);
98140
}
99141

100-
private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails,
101-
SslBundles sslBundles) {
142+
private ClusterEnvironment.Builder initializeEnvironmentBuilder(SslBundles sslBundles) {
102143
ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
103144
Timeouts timeouts = this.properties.getEnv().getTimeouts();
104145
builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue())

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* @author Yulin Qin
3030
* @author Brian Clozel
3131
* @author Michael Nitschinger
32+
* @author Scott Frederick
3233
* @since 1.4.0
3334
*/
3435
@ConfigurationProperties(prefix = "spring.couchbase")
@@ -49,6 +50,8 @@ public class CouchbaseProperties {
4950
*/
5051
private String password;
5152

53+
private final Authentication authentication = new Authentication();
54+
5255
private final Env env = new Env();
5356

5457
public String getConnectionString() {
@@ -75,10 +78,116 @@ public void setPassword(String password) {
7578
this.password = password;
7679
}
7780

81+
public Authentication getAuthentication() {
82+
return this.authentication;
83+
}
84+
7885
public Env getEnv() {
7986
return this.env;
8087
}
8188

89+
public static class Authentication {
90+
91+
private final Pem pem = new Pem();
92+
93+
private final Jks jks = new Jks();
94+
95+
public Pem getPem() {
96+
return this.pem;
97+
}
98+
99+
public Jks getJks() {
100+
return this.jks;
101+
}
102+
103+
public static class Pem {
104+
105+
/**
106+
* PEM-formatted certificates for certificate-based cluster authentication.
107+
*/
108+
private String certificates;
109+
110+
/**
111+
* PEM-formatted private key for certificate-based cluster authentication.
112+
*/
113+
private String privateKey;
114+
115+
/**
116+
* Private key password for certificate-based cluster authentication.
117+
*/
118+
private String privateKeyPassword;
119+
120+
public String getCertificates() {
121+
return this.certificates;
122+
}
123+
124+
public void setCertificates(String certificates) {
125+
this.certificates = certificates;
126+
}
127+
128+
public String getPrivateKey() {
129+
return this.privateKey;
130+
}
131+
132+
public void setPrivateKey(String privateKey) {
133+
this.privateKey = privateKey;
134+
}
135+
136+
public String getPrivateKeyPassword() {
137+
return this.privateKeyPassword;
138+
}
139+
140+
public void setPrivateKeyPassword(String privateKeyPassword) {
141+
this.privateKeyPassword = privateKeyPassword;
142+
}
143+
144+
}
145+
146+
public static class Jks {
147+
148+
/**
149+
* Java KeyStore location for certificate-based cluster authentication.
150+
*/
151+
private String location;
152+
153+
/**
154+
* Java KeyStore password for certificate-based cluster authentication.
155+
*/
156+
private String password;
157+
158+
/**
159+
* Private key password for certificate-based cluster authentication.
160+
*/
161+
private String privateKeyPassword;
162+
163+
public String getLocation() {
164+
return this.location;
165+
}
166+
167+
public void setLocation(String location) {
168+
this.location = location;
169+
}
170+
171+
public String getPassword() {
172+
return this.password;
173+
}
174+
175+
public void setPassword(String password) {
176+
this.password = password;
177+
}
178+
179+
public String getPrivateKeyPassword() {
180+
return this.privateKeyPassword;
181+
}
182+
183+
public void setPrivateKeyPassword(String privateKeyPassword) {
184+
this.privateKeyPassword = privateKeyPassword;
185+
}
186+
187+
}
188+
189+
}
190+
82191
public static class Env {
83192

84193
private final Io io = new Io();

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
import java.util.Set;
2222
import java.util.function.Consumer;
2323

24+
import com.couchbase.client.core.env.Authenticator;
25+
import com.couchbase.client.core.env.CertificateAuthenticator;
2426
import com.couchbase.client.core.env.IoConfig;
27+
import com.couchbase.client.core.env.PasswordAuthenticator;
2528
import com.couchbase.client.core.env.SecurityConfig;
2629
import com.couchbase.client.core.env.TimeoutConfig;
2730
import com.couchbase.client.java.Cluster;
@@ -54,6 +57,7 @@
5457
* @author Moritz Halbritter
5558
* @author Andy Wilkinson
5659
* @author Phillip Webb
60+
* @author Scott Frederick
5761
*/
5862
class CouchbaseAutoConfigurationTests {
5963

@@ -63,6 +67,7 @@ class CouchbaseAutoConfigurationTests {
6367
@Test
6468
void connectionStringIsRequired() {
6569
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ClusterEnvironment.class)
70+
.doesNotHaveBean(Authenticator.class)
6671
.doesNotHaveBean(Cluster.class));
6772
}
6873

@@ -79,6 +84,7 @@ void shouldUseCustomConnectionDetailsWhenDefined() {
7984
.run((context) -> {
8085
assertThat(context).hasSingleBean(ClusterEnvironment.class)
8186
.hasSingleBean(Cluster.class)
87+
.hasSingleBean(PasswordAuthenticator.class)
8288
.hasSingleBean(CouchbaseConnectionDetails.class)
8389
.doesNotHaveBean(PropertiesCouchbaseConnectionDetails.class);
8490
Cluster cluster = context.getBean(Cluster.class);
@@ -94,19 +100,24 @@ void connectionStringCreateEnvironmentAndCluster() {
94100
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
95101
.withPropertyValues("spring.couchbase.connection-string=localhost")
96102
.run((context) -> {
97-
assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class);
103+
assertThat(context).hasSingleBean(ClusterEnvironment.class)
104+
.hasSingleBean(Authenticator.class)
105+
.hasSingleBean(Cluster.class);
106+
assertThat(context).doesNotHaveBean("couchbaseAuthenticator");
98107
assertThat(context.getBean(Cluster.class))
99108
.isSameAs(context.getBean(CouchbaseTestConfiguration.class).couchbaseCluster());
100109
});
101110
}
102111

103112
@Test
104-
void connectionDetailsShouldOverrideProperties() {
113+
void connectionDetailsOverridesProperties() {
105114
this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails)
106115
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=a-user",
107116
"spring.couchbase.password=a-password")
108117
.run((context) -> {
109-
assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class);
118+
assertThat(context).hasSingleBean(ClusterEnvironment.class)
119+
.hasSingleBean(PasswordAuthenticator.class)
120+
.hasSingleBean(Cluster.class);
110121
Cluster cluster = context.getBean(Cluster.class);
111122
assertThat(cluster.core()).extracting("connectionString.hosts")
112123
.asInstanceOf(InstanceOfAssertFactories.LIST)
@@ -243,6 +254,41 @@ void customizeEnvWithCustomCouchbaseConfiguration() {
243254
});
244255
}
245256

257+
@Test
258+
void passwordAuthenticationWithUsernameAndPassword() {
259+
this.contextRunner
260+
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=user",
261+
"spring.couchbase.password=secret")
262+
.run((context) -> assertThat(context).hasSingleBean(PasswordAuthenticator.class));
263+
}
264+
265+
@Test
266+
void certificateAuthenticationWithPemPrivateKeyAndCertificate() {
267+
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost",
268+
"spring.couchbase.env.ssl.enabled=true",
269+
"spring.couchbase.authentication.pem.private-key=classpath:org/springframework/boot/autoconfigure/ssl/key2.pem",
270+
"spring.couchbase.authentication.pem.certificates=classpath:org/springframework/boot/autoconfigure/ssl/key2.crt")
271+
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
272+
}
273+
274+
@Test
275+
void certificateAuthenticationWithJavaKeyStore() {
276+
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost",
277+
"spring.couchbase.env.ssl.enabled=true",
278+
"spring.couchbase.authentication.jks.location=classpath:org/springframework/boot/autoconfigure/ssl/keystore.jks",
279+
"spring.couchbase.authentication.jks.password=secret")
280+
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
281+
}
282+
283+
@Test
284+
void failsWithMissingAuthentication() {
285+
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> {
286+
assertThat(context).hasFailed();
287+
assertThat(context).getFailure()
288+
.hasMessageContaining("Couchbase authentication requires username and password, or certificates");
289+
});
290+
}
291+
246292
private CouchbaseConnectionDetails couchbaseConnectionDetails() {
247293
return new CouchbaseConnectionDetails() {
248294

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.autoconfigure.couchbase;
1818

19+
import com.couchbase.client.core.env.Authenticator;
1920
import com.couchbase.client.java.Cluster;
2021

2122
import org.springframework.context.annotation.Bean;
@@ -27,15 +28,23 @@
2728
* Test configuration for couchbase that mocks access.
2829
*
2930
* @author Stephane Nicoll
31+
* @author Scott Frederick
3032
*/
3133
@Configuration(proxyBeanMethods = false)
3234
class CouchbaseTestConfiguration {
3335

3436
private final Cluster cluster = mock(Cluster.class);
3537

38+
private final Authenticator authenticator = mock(Authenticator.class);
39+
3640
@Bean
3741
Cluster couchbaseCluster() {
3842
return this.cluster;
3943
}
4044

45+
@Bean
46+
Authenticator couchbaseAuth() {
47+
return this.authenticator;
48+
}
49+
4150
}

0 commit comments

Comments
 (0)