Skip to content
Merged
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
25 changes: 23 additions & 2 deletions duo-universal-sdk/src/main/java/com/duosecurity/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ public static class Builder {
private final String redirectUri;
private Boolean useDuoCodeAttribute;
private String[] caCerts;
private boolean caPinningDisabled;
private String userAgent;

private static final String[] DEFAULT_CA_CERTS = {
Expand Down Expand Up @@ -361,14 +362,20 @@ public Builder(String clientId, String clientSecret, String proxyHost,
public Client build() throws DuoException {
validateClientParams(clientId, clientSecret, apiHost, redirectUri);

if (caPinningDisabled && caCerts != DEFAULT_CA_CERTS) {
throw new DuoException(
"Cannot both disable CA pinning and provide custom certificates");
}

Client client = new Client();
client.clientId = clientId;
client.clientSecret = clientSecret;
client.apiHost = apiHost;
client.redirectUri = redirectUri;
client.useDuoCodeAttribute = useDuoCodeAttribute;
client.userAgent = userAgent;
client.duoConnector = new DuoConnector(apiHost, proxyHost, proxyPort, caCerts);
client.duoConnector = new DuoConnector(apiHost, proxyHost, proxyPort,
caPinningDisabled ? null : caCerts);

return client;
}
Expand All @@ -377,7 +384,7 @@ public Client build() throws DuoException {
* Optionally use custom CA Certificates when validating connections to Duo.
*
* @param userCaCerts List of CA Certificates to use
*
*
* @return the Builder
*/
public Builder setCACerts(String[] userCaCerts) {
Expand All @@ -387,6 +394,20 @@ public Builder setCACerts(String[] userCaCerts) {
return this;
}

/**
* Disable certificate pinning for connections to Duo.
* When disabled, TLS verification still occurs using the system's default
* trusted certificate authorities, but the bundled CA pin set is not enforced.
*
* <p>This option is mutually exclusive with {@link #setCACerts(String[])}.
*
* @return the Builder
*/
public Builder disableCaPinning() {
this.caPinningDisabled = true;
return this;
}

/**
* Optionally toggle the returned authorization parameter to use duo_code vs code.
* Defaults true to use duo_code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,32 @@ public DuoConnector(String apiHost, String[] caCerts) throws DuoException {
* @param apiHost This value is the api host provided by Duo in the admin panel.
* @param proxyHost This value is the proxy server hostname
* @param proxyPort This value is the proxy server port
* @param caCerts CA Certificates used to connect to Duo
* @param caCerts CA Certificates used to connect to Duo, or null to disable pinning
*
* @throws DuoException For issues getting and validating the URL
*/
public DuoConnector(String apiHost, String proxyHost, Integer proxyPort, String[] caCerts)
throws DuoException {
CertificatePinner duoCertificatePinner = new CertificatePinner.Builder()
.add(apiHost, caCerts).build();
CertificatePinner certificatePinner;
if (caCerts != null) {
certificatePinner = new CertificatePinner.Builder()
.add(apiHost, caCerts).build();
} else {
certificatePinner = CertificatePinner.DEFAULT;
}
ConnectionPool connectionPool = new ConnectionPool(
MAX_IDLE_CONNECTIONS, CONNECTION_KEEP_ALIVE_SECONDS, TimeUnit.SECONDS);
OkHttpClient client;
if (proxyHost != null && proxyPort != null) {
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
client = new OkHttpClient.Builder()
.certificatePinner(duoCertificatePinner)
.certificatePinner(certificatePinner)
.connectionPool(connectionPool)
.proxy(proxy)
.build();
} else {
client = new OkHttpClient.Builder()
.certificatePinner(duoCertificatePinner)
.certificatePinner(certificatePinner)
.connectionPool(connectionPool)
.build();
}
Expand Down
34 changes: 34 additions & 0 deletions duo-universal-sdk/src/test/java/com/duosecurity/ClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,40 @@ void custom_useragent() throws DuoException {
assertTrue(sentUserAgent.startsWith("duo_universal_java") && sentUserAgent.endsWith(appendedUserAgent));
}

@Test
void disableCaPinning_builds_successfully() throws DuoException {
Client client = new Client.Builder(CLIENT_ID, CLIENT_SECRET, API_HOST, HTTPS_REDIRECT_URI)
.disableCaPinning()
.build();
assertNotNull(client);
}

@Test
void disableCaPinning_with_custom_certs_throws_exception() {
try {
new Client.Builder(CLIENT_ID, CLIENT_SECRET, API_HOST, HTTPS_REDIRECT_URI)
.setCACerts(new String[]{"sha256/test"})
.disableCaPinning()
.build();
Assertions.fail();
} catch (DuoException e) {
assertTrue(e.getMessage().contains("Cannot both disable CA pinning and provide custom certificates"));
}
}

@Test
void disableCaPinning_then_custom_certs_throws_exception() {
try {
new Client.Builder(CLIENT_ID, CLIENT_SECRET, API_HOST, HTTPS_REDIRECT_URI)
.disableCaPinning()
.setCACerts(new String[]{"sha256/test"})
.build();
Assertions.fail();
} catch (DuoException e) {
assertTrue(e.getMessage().contains("Cannot both disable CA pinning and provide custom certificates"));
}
}

@Test
void legacy_constructors_match() throws DuoException {
// Create clients using the old deprecated constructors and check that their fields are the same as one created using the builder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.duosecurity.exception.DuoException;
import com.duosecurity.model.HealthCheckResponse;
import com.duosecurity.model.TokenResponse;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand All @@ -11,6 +13,7 @@
import retrofit2.Retrofit;

import java.io.IOException;
import java.lang.reflect.Field;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -62,6 +65,43 @@ void duoHealthcheck_network_failure() throws IOException, DuoException {
}
}

@Test
void constructor_with_null_caCerts_disables_pinning() throws Exception {
DuoConnector duoConnector = new DuoConnector(API_HOST, null);
OkHttpClient client = getOkHttpClient(duoConnector);
assertTrue(getPins(client.certificatePinner()).isEmpty());
}

@Test
void constructor_with_null_caCerts_and_proxy_disables_pinning() throws Exception {
DuoConnector duoConnector = new DuoConnector(API_HOST, "proxy.example.com", 8080, null);
OkHttpClient client = getOkHttpClient(duoConnector);
assertTrue(getPins(client.certificatePinner()).isEmpty());
}

@Test
void constructor_with_caCerts_enables_pinning() throws Exception {
DuoConnector duoConnector = new DuoConnector(API_HOST, CA_CERT);
OkHttpClient client = getOkHttpClient(duoConnector);
assertFalse(getPins(client.certificatePinner()).isEmpty());
}

private OkHttpClient getOkHttpClient(DuoConnector connector) throws Exception {
Field retrofitField = DuoConnector.class.getDeclaredField("retrofit");
retrofitField.setAccessible(true);
Retrofit retrofit = (Retrofit) retrofitField.get(connector);
Field clientField = Retrofit.class.getDeclaredField("callFactory");
clientField.setAccessible(true);
return (OkHttpClient) clientField.get(retrofit);
}

@SuppressWarnings("unchecked")
private java.util.Set<?> getPins(CertificatePinner pinner) throws Exception {
Field pinsField = CertificatePinner.class.getDeclaredField("pins");
pinsField.setAccessible(true);
return (java.util.Set<?>) pinsField.get(pinner);
}

@Test
void exchangeAuthorizationCodeFor2FAResult() throws IOException, DuoException {
DuoConnector duoConnector = new DuoConnector(API_HOST, CA_CERT);
Expand Down
Loading