From 59851e31250664bc47549f3736141fefa8440798 Mon Sep 17 00:00:00 2001 From: Chad Hilton Date: Tue, 24 Oct 2023 14:39:24 -0400 Subject: [PATCH 1/4] Add proxy server support --- .../src/main/java/com/duosecurity/Client.java | 71 ++++++++++++++++++- .../com/duosecurity/service/DuoConnector.java | 47 +++++++----- .../duosecurity/service/DuoConnectorTest.java | 2 +- 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/duo-universal-sdk/src/main/java/com/duosecurity/Client.java b/duo-universal-sdk/src/main/java/com/duosecurity/Client.java index 7c35887..fb16749 100644 --- a/duo-universal-sdk/src/main/java/com/duosecurity/Client.java +++ b/duo-universal-sdk/src/main/java/com/duosecurity/Client.java @@ -53,6 +53,10 @@ public class Client { private Boolean useDuoCodeAttribute; + private String proxyHost; + + private Integer proxyPort; + protected DuoConnector duoConnector; private String userAgent; @@ -111,10 +115,49 @@ public Client(String clientId, String clientSecret, String apiHost, this.userAgent = client.userAgent; } + /** + * Legacy constructor which allows specifying custom CaCerts. + * + * @param clientId This value is the client id provided by Duo in the admin + * panel. + * @param clientSecret This value is the client secret provided by Duo in the + * admin panel. + * @param apiHost This value is the api host provided by Duo in the admin + * panel. + * @param redirectUri This value is the uri which Duo should redirect to after + * 2FA is completed. + * @param proxyHost This value is the hostname of the proxy server + * @param proxyPort This value is the port number of the proxy server + * @param userCaCerts This value is a list of CA Certificates used to validate + * connections to Duo + * + * @throws DuoException For problems building the client + * + * @deprecated The constructors are deprecated. Prefer the + * {@link Client.Builder} for instantiating Clients + */ + @Deprecated + public Client(String clientId, String clientSecret, String apiHost, String redirectUri, String proxyHost, Integer proxyPort, + String[] userCaCerts) throws DuoException { + Client client = new Builder(clientId, clientSecret, proxyHost, proxyPort, apiHost, redirectUri).setCACerts(userCaCerts) + .build(); + this.clientId = client.clientId; + this.clientSecret = client.clientSecret; + this.apiHost = client.apiHost; + this.redirectUri = client.redirectUri; + this.useDuoCodeAttribute = client.useDuoCodeAttribute; + this.duoConnector = client.duoConnector; + this.userAgent = client.userAgent; + this.proxyHost = client.proxyHost; + this.proxyPort = client.proxyPort; + } + public static class Builder { private final String clientId; private final String clientSecret; private final String apiHost; + private String proxyHost; + private Integer proxyPort; private final String redirectUri; private Boolean useDuoCodeAttribute; private String[] caCerts; @@ -162,6 +205,32 @@ public Builder(String clientId, String clientSecret, String apiHost, this.userAgent = computeUserAgent(); } + /** + * Builder. + * + * @param clientId This value is the client id provided by Duo in the admin + * panel. + * @param clientSecret This value is the client secret provided by Duo in the + * admin panel. + * @param proxyHost This value is the hostname of the proxy server + * @param proxyPort This value is the port number of the proxy server + * @param apiHost This value is the api host provided by Duo in the admin + * panel. + * @param redirectUri This value is the uri which Duo should redirect to after + * 2FA is completed. + */ + public Builder(String clientId, String clientSecret, String proxyHost, Integer proxyPort, String apiHost, String redirectUri) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.apiHost = apiHost; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.redirectUri = redirectUri; + this.caCerts = DEFAULT_CA_CERTS; + this.useDuoCodeAttribute = true; + this.userAgent = computeUserAgent(); + } + /** * Build the client object. * @@ -179,7 +248,7 @@ public Client build() throws DuoException { client.redirectUri = redirectUri; client.useDuoCodeAttribute = useDuoCodeAttribute; client.userAgent = userAgent; - client.duoConnector = new DuoConnector(apiHost, caCerts); + client.duoConnector = new DuoConnector(apiHost, proxyHost, proxyPort, caCerts); return client; } diff --git a/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java b/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java index b43429d..1213a11 100644 --- a/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java +++ b/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java @@ -2,6 +2,9 @@ import static com.duosecurity.Utils.getAndValidateUrl; +import java.net.InetSocketAddress; +import java.net.Proxy; + import com.duosecurity.exception.DuoException; import com.duosecurity.model.HealthCheckResponse; import com.duosecurity.model.TokenResponse; @@ -22,24 +25,35 @@ public class DuoConnector { /** * DuoConnector Constructor. * - * @param apiHost This value is the api host provided by Duo in the admin panel. - * @param caCerts CA Certificates used to connect to Duo + * @param apiHost This value is the api host provided by Duo in the admin panel. + * @param caCerts CA Certificates used to connect to Duo * - * @throws DuoException For issues getting and validating the URL + * @throws DuoException For issues getting and validating the URL */ public DuoConnector(String apiHost, String[] caCerts) throws DuoException { - CertificatePinner duoCertificatePinner = new CertificatePinner.Builder() - .add(apiHost, caCerts) - .build(); + this(apiHost, null, null, caCerts); + } - OkHttpClient client = new OkHttpClient.Builder().certificatePinner( - duoCertificatePinner).build(); + /** + * DuoConnector Constructor. + * + * @param apiHost This value is the api host provided by Duo in the admin panel. + * @param caCerts CA Certificates used to connect to Duo + * + * @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(); + OkHttpClient client; + if (proxyHost != null && proxyPort != null) { + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + client = new OkHttpClient.Builder().certificatePinner(duoCertificatePinner).proxy(proxy).build(); + } else { + client = new OkHttpClient.Builder().certificatePinner(duoCertificatePinner).build(); + } - retrofit = new Retrofit.Builder() - .baseUrl(getAndValidateUrl(apiHost, "").toString()) - .addConverterFactory(JacksonConverterFactory.create()) - .client(client) - .build(); + retrofit = new Retrofit.Builder().baseUrl(getAndValidateUrl(apiHost, "").toString()) + .addConverterFactory(JacksonConverterFactory.create()).client(client).build(); } /** @@ -91,12 +105,7 @@ public TokenResponse exchangeAuthorizationCodeFor2FAResult(String userAgent, Str try { Response response = callSync.execute(); if (response.code() != SUCCESS_STATUS_CODE || response.body() == null) { - String message = response.message(); - if (response.errorBody() != null) { - throw new DuoException(String.format("msg=%s, msg_detail=%s", - message, response.errorBody().string())); - } - throw new DuoException(message); + throw new DuoException(response.message()); } return response.body(); } catch (IOException e) { diff --git a/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java b/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java index 02dcfda..33c0b2e 100644 --- a/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java +++ b/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java @@ -149,7 +149,7 @@ void exchangeAuthorizationCodeFor2FAResult_null_body() throws IOException, DuoEx Assertions.fail(); } catch (DuoException e) { // Response.success() is the error message because that's the default message when manually crafting - // a successful (200) response. This still verifies we're properly creating the DuoExpection. + // a successful (200) response. This still verifies we're properly creating the DuoException. assertEquals("Response.success()", e.getMessage()); } } From ba8967238b320bb813c3ca581900014114271c1b Mon Sep 17 00:00:00 2001 From: Chad Hilton Date: Tue, 24 Oct 2023 16:00:18 -0400 Subject: [PATCH 2/4] Update tests to reflect new Exception response format --- .../src/main/java/com/duosecurity/Client.java | 15 +++++---- .../com/duosecurity/service/DuoConnector.java | 33 ++++++++++++------- .../duosecurity/service/DuoConnectorTest.java | 2 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/duo-universal-sdk/src/main/java/com/duosecurity/Client.java b/duo-universal-sdk/src/main/java/com/duosecurity/Client.java index fb16749..320ce70 100644 --- a/duo-universal-sdk/src/main/java/com/duosecurity/Client.java +++ b/duo-universal-sdk/src/main/java/com/duosecurity/Client.java @@ -122,14 +122,12 @@ public Client(String clientId, String clientSecret, String apiHost, * panel. * @param clientSecret This value is the client secret provided by Duo in the * admin panel. - * @param apiHost This value is the api host provided by Duo in the admin - * panel. + * @param apiHost This value is the api host provided by Duo in the admin panel. * @param redirectUri This value is the uri which Duo should redirect to after * 2FA is completed. * @param proxyHost This value is the hostname of the proxy server * @param proxyPort This value is the port number of the proxy server - * @param userCaCerts This value is a list of CA Certificates used to validate - * connections to Duo + * @param userCaCerts This value is a list of CA Certificates used to validate connections to Duo * * @throws DuoException For problems building the client * @@ -137,9 +135,11 @@ public Client(String clientId, String clientSecret, String apiHost, * {@link Client.Builder} for instantiating Clients */ @Deprecated - public Client(String clientId, String clientSecret, String apiHost, String redirectUri, String proxyHost, Integer proxyPort, + public Client(String clientId, String clientSecret, String apiHost, + String redirectUri, String proxyHost, Integer proxyPort, String[] userCaCerts) throws DuoException { - Client client = new Builder(clientId, clientSecret, proxyHost, proxyPort, apiHost, redirectUri).setCACerts(userCaCerts) + Client client = new Builder(clientId, clientSecret, proxyHost, proxyPort, apiHost, redirectUri) + .setCACerts(userCaCerts) .build(); this.clientId = client.clientId; this.clientSecret = client.clientSecret; @@ -219,7 +219,8 @@ public Builder(String clientId, String clientSecret, String apiHost, * @param redirectUri This value is the uri which Duo should redirect to after * 2FA is completed. */ - public Builder(String clientId, String clientSecret, String proxyHost, Integer proxyPort, String apiHost, String redirectUri) { + public Builder(String clientId, String clientSecret, String proxyHost, + Integer proxyPort, String apiHost, String redirectUri) { this.clientId = clientId; this.clientSecret = clientSecret; this.apiHost = apiHost; diff --git a/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java b/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java index 1213a11..2c87dd5 100644 --- a/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java +++ b/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java @@ -2,13 +2,12 @@ import static com.duosecurity.Utils.getAndValidateUrl; -import java.net.InetSocketAddress; -import java.net.Proxy; - import com.duosecurity.exception.DuoException; import com.duosecurity.model.HealthCheckResponse; import com.duosecurity.model.TokenResponse; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; import okhttp3.CertificatePinner; import okhttp3.OkHttpClient; import retrofit2.Call; @@ -38,22 +37,34 @@ public DuoConnector(String apiHost, String[] caCerts) throws DuoException { * DuoConnector Constructor. * * @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 * * @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(); + public DuoConnector(String apiHost, String proxyHost, Integer proxyPort, String[] caCerts) + throws DuoException { + CertificatePinner duoCertificatePinner = new CertificatePinner.Builder() + .add(apiHost, caCerts).build(); OkHttpClient client; if (proxyHost != null && proxyPort != null) { Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); - client = new OkHttpClient.Builder().certificatePinner(duoCertificatePinner).proxy(proxy).build(); + client = new OkHttpClient.Builder() + .certificatePinner(duoCertificatePinner) + .proxy(proxy) + .build(); } else { - client = new OkHttpClient.Builder().certificatePinner(duoCertificatePinner).build(); + client = new OkHttpClient.Builder() + .certificatePinner(duoCertificatePinner) + .build(); } - retrofit = new Retrofit.Builder().baseUrl(getAndValidateUrl(apiHost, "").toString()) - .addConverterFactory(JacksonConverterFactory.create()).client(client).build(); + retrofit = new Retrofit.Builder() + .baseUrl(getAndValidateUrl(apiHost, "").toString()) + .addConverterFactory(JacksonConverterFactory.create()) + .client(client) + .build(); } /** @@ -67,7 +78,7 @@ public DuoConnector(String apiHost, String proxyHost, Integer proxyPort, String[ * @throws DuoException For issues sending or receiving the request */ public HealthCheckResponse duoHealthcheck(String clientId, String clientAssertion) - throws DuoException { + throws DuoException { DuoService service = retrofit.create(DuoService.class); Call callSync = service.duoHealthCheck(clientId, clientAssertion); try { @@ -98,7 +109,7 @@ public TokenResponse exchangeAuthorizationCodeFor2FAResult(String userAgent, Str String duoCode, String redirectUri, String clientAssertionType, String clientAssertion) - throws DuoException { + throws DuoException { DuoService service = retrofit.create(DuoService.class); Call callSync = service.exchangeAuthorizationCodeFor2FAResult(userAgent, grantType, duoCode, redirectUri, clientAssertionType, clientAssertion); diff --git a/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java b/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java index 33c0b2e..1666a7f 100644 --- a/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java +++ b/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java @@ -125,7 +125,7 @@ void exchangeAuthorizationCodeFor2FAResult_error_code() throws IOException, DuoE "client_assertion_type", "client_assertion"); Assertions.fail(); } catch (DuoException e) { - assertEquals("msg=Response.error(), msg_detail=", e.getMessage()); + assertEquals("Response.error()", e.getMessage()); } } From e2f95c2c5c1b09c9625b3d464e80d3df398ae39b Mon Sep 17 00:00:00 2001 From: Chad Hilton Date: Tue, 31 Oct 2023 10:42:46 -0400 Subject: [PATCH 3/4] Update DuoConnector.java Restore code to display response.errorBody if it's not null. --- .../main/java/com/duosecurity/service/DuoConnector.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java b/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java index 2c87dd5..c73f272 100644 --- a/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java +++ b/duo-universal-sdk/src/main/java/com/duosecurity/service/DuoConnector.java @@ -116,7 +116,12 @@ public TokenResponse exchangeAuthorizationCodeFor2FAResult(String userAgent, Str try { Response response = callSync.execute(); if (response.code() != SUCCESS_STATUS_CODE || response.body() == null) { - throw new DuoException(response.message()); + String message = response.message(); + if (response.errorBody() != null) { + throw new DuoException(String.format("msg=%s, msg_detail=%s", + message, response.errorBody().string())); + } + throw new DuoException(message); } return response.body(); } catch (IOException e) { From 1b67a82c7a80af5175c647373842d6f57357e3ee Mon Sep 17 00:00:00 2001 From: Chad Hilton Date: Tue, 31 Oct 2023 11:18:36 -0400 Subject: [PATCH 4/4] Update DuoConnectorTest.java Restore exception message format to match the format in the exchangeAuthorizationCodeFor2FAResult_error_code test. --- .../src/test/java/com/duosecurity/service/DuoConnectorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java b/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java index 1666a7f..33c0b2e 100644 --- a/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java +++ b/duo-universal-sdk/src/test/java/com/duosecurity/service/DuoConnectorTest.java @@ -125,7 +125,7 @@ void exchangeAuthorizationCodeFor2FAResult_error_code() throws IOException, DuoE "client_assertion_type", "client_assertion"); Assertions.fail(); } catch (DuoException e) { - assertEquals("Response.error()", e.getMessage()); + assertEquals("msg=Response.error(), msg_detail=", e.getMessage()); } }