Skip to content

Add reactive certificate mapping support #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/org/cloudfoundry/router/CertificateLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2017-2020 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.cloudfoundry.router;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

class CertificateLoader {

public static final String HEADER_NAME = "X-Forwarded-Client-Cert";
private final CertificateFactory certificateFactory;

public CertificateLoader(CertificateFactory certificateFactory) {
this.certificateFactory = certificateFactory;
}

public List<X509Certificate> getCertificates(Iterable<String> headerValues) throws CertificateException, IOException {
List<X509Certificate> certificates = new ArrayList<>();

for (String rawCertificate : getRawCertificates(headerValues)) {
try (InputStream in = new ByteArrayInputStream(decodeHeader(rawCertificate))) {
certificates.add((X509Certificate) this.certificateFactory.generateCertificate(in));
}
}

return certificates;
}

private byte[] decodeHeader(String rawCertificate) {
try {
return Base64.getDecoder().decode(rawCertificate);
} catch (IllegalArgumentException e1) {
try {
return URLDecoder.decode(rawCertificate, "utf-8").getBytes();
} catch (UnsupportedEncodingException e2) {
throw new IllegalArgumentException("Header contains value that is neither base64 nor url encoded");
}
}
}

private List<String> getRawCertificates(Iterable<String> headerValues) {
if (headerValues == null) {
return Collections.emptyList();
}

Iterator<String> candidates = headerValues.iterator();
List<String> rawCertificates = new ArrayList<>();
while (candidates.hasNext()) {
String candidate = candidates.next();

if (hasMultipleCertificates(candidate)) {
rawCertificates.addAll(Arrays.asList(candidate.split(",")));
} else {
rawCertificates.add(candidate);
}
}

return rawCertificates;
}

private boolean hasMultipleCertificates(String candidate) {
return candidate.indexOf(',') != -1;
}

}
59 changes: 4 additions & 55 deletions src/main/java/org/cloudfoundry/router/ClientCertificateMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,12 @@ final class ClientCertificateMapper implements Filter {

static final String ATTRIBUTE = "javax.servlet.request.X509Certificate";

static final String HEADER = "X-Forwarded-Client-Cert";

private final Logger logger = Logger.getLogger(this.getClass().getName());

private final CertificateFactory certificateFactory;
private final CertificateLoader certificateLoader;

ClientCertificateMapper() throws CertificateException {
this.certificateFactory = CertificateFactory.getInstance("X.509");
this.certificateLoader = new CertificateLoader(CertificateFactory.getInstance("X.509"));
}

@Override
Expand All @@ -66,7 +64,8 @@ public void destroy() {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
try {
List<X509Certificate> certificates = getCertificates((HttpServletRequest) request);
Enumeration<String> header = ((HttpServletRequest) request).getHeaders(CertificateLoader.HEADER_NAME);
List<X509Certificate> certificates = this.certificateLoader.getCertificates(Collections.list(header));

if (!certificates.isEmpty()) {
request.setAttribute(ATTRIBUTE, certificates.toArray(new X509Certificate[0]));
Expand All @@ -83,54 +82,4 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
public void init(FilterConfig filterConfig) {

}

private byte[] decodeHeader(String rawCertificate) {
try {
return Base64.getDecoder().decode(rawCertificate);
} catch (IllegalArgumentException e1) {
try {
return URLDecoder.decode(rawCertificate, "utf-8").getBytes();
} catch (UnsupportedEncodingException e2) {
throw new IllegalArgumentException("Header contains value that is neither base64 nor url encoded");
}
}
}

private List<X509Certificate> getCertificates(HttpServletRequest request) throws CertificateException, IOException {
List<X509Certificate> certificates = new ArrayList<>();

for (String rawCertificate : getRawCertificates(request)) {
try (InputStream in = new ByteArrayInputStream(decodeHeader(rawCertificate))) {
certificates.add((X509Certificate) this.certificateFactory.generateCertificate(in));
}
}

return certificates;
}

private List<String> getRawCertificates(HttpServletRequest request) {
Enumeration<String> candidates = request.getHeaders(HEADER);

if (candidates == null) {
return Collections.emptyList();
}

List<String> rawCertificates = new ArrayList<>();
while (candidates.hasMoreElements()) {
String candidate = candidates.nextElement();

if (hasMultipleCertificates(candidate)) {
rawCertificates.addAll(Arrays.asList(candidate.split(",")));
} else {
rawCertificates.add(candidate);
}
}

return rawCertificates;
}

private boolean hasMultipleCertificates(String candidate) {
return candidate.indexOf(',') != -1;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2017-2020 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.cloudfoundry.router;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.security.cert.CertificateException;

public class ReactiveClientCertificateMapper implements WebFilter {

private final Logger logger = LoggerFactory.getLogger(ReactiveClientCertificateMapper.class);

private final CertificateLoader certificateLoader;

public ReactiveClientCertificateMapper(CertificateLoader certificateLoader) {
this.certificateLoader = certificateLoader;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
try {
final SslInfoRequestDecorator requestDecorator = new SslInfoRequestDecorator(this.certificateLoader, exchange.getRequest());
final ServerWebExchange exchangeWithSslInfo = exchange.mutate().request(requestDecorator).build();
return chain.filter(exchangeWithSslInfo);
} catch (CertificateException e) {
this.logger.warn("Unable to parse certificates in X-Forwarded-Client-Cert");
} catch (IOException e) {
return Mono.error(e);
}

return chain.filter(exchange);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2017-2020 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.cloudfoundry.router;

import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.WebFilter;

import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
@Configuration
public class ReactiveClientCertificateMapperAutoConfiguration {

@Bean
WebFilter reactiveCertificateMapper() throws CertificateException {
CertificateLoader certificateLoader = new CertificateLoader(CertificateFactory.getInstance("X.509"));
return new ReactiveClientCertificateMapper(certificateLoader);
}
}
42 changes: 42 additions & 0 deletions src/main/java/org/cloudfoundry/router/SimpleSslInfoHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2017-2020 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.cloudfoundry.router;

import org.springframework.http.server.reactive.SslInfo;

import java.security.cert.X509Certificate;

public class SimpleSslInfoHolder implements SslInfo {

private final String sessionId;
private final X509Certificate[] peerCertificates;

public SimpleSslInfoHolder(String sessionId, X509Certificate[] peerCertificates) {
this.sessionId = sessionId;
this.peerCertificates = peerCertificates;
}

@Override
public String getSessionId() {
return this.sessionId;
}

@Override
public X509Certificate[] getPeerCertificates() {
return this.peerCertificates;
}
}
59 changes: 59 additions & 0 deletions src/main/java/org/cloudfoundry/router/SslInfoRequestDecorator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2017-2020 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.cloudfoundry.router;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.SslInfo;

import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.List;

public class SslInfoRequestDecorator extends ServerHttpRequestDecorator {

private final Logger logger = LoggerFactory.getLogger(SslInfoRequestDecorator.class);

private final CertificateLoader certificateLoader;

private final SslInfo sslInfo;

public SslInfoRequestDecorator(CertificateLoader certificateLoader, ServerHttpRequest delegate) throws CertificateException, IOException {
super(delegate);

this.certificateLoader = certificateLoader;
final List<String> headers = delegate.getHeaders().get(CertificateLoader.HEADER_NAME);

if (delegate.getSslInfo() != null || headers == null || headers.isEmpty()) {
logger.debug("Original request contains SslInfo, skipping certificate loading from header");
this.sslInfo = delegate.getSslInfo();
}
else {
logger.debug("Original request does not contain SslInfo, loading certificates from header");
this.sslInfo = new SimpleSslInfoHolder(null, certificateLoader.getCertificates(headers).toArray(new X509Certificate[0]));
}
}

@Override
public SslInfo getSslInfo() {
return this.sslInfo;
}
}
2 changes: 1 addition & 1 deletion src/main/resources/META-INF/spring.factories
Original file line number Diff line number Diff line change
@@ -1 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.cloudfoundry.router.ClientCertificateMapperAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.cloudfoundry.router.ClientCertificateMapperAutoConfiguration,org.cloudfoundry.router.ReactiveClientCertificateMapperAutoConfiguration
Loading