From d140d4aeefee737b468662f85965d8922550496e Mon Sep 17 00:00:00 2001 From: Mihir Pradhan Date: Tue, 21 Jul 2026 09:46:34 -0500 Subject: [PATCH 1/2] Add CA bundle version and pinning status to user agent string Co-Authored-By: Claude Opus 4.6 --- .../java/com/duosecurity/client/Http.java | 11 +++- .../duosecurity/client/HttpUserAgentTest.java | 54 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java diff --git a/duo-client/src/main/java/com/duosecurity/client/Http.java b/duo-client/src/main/java/com/duosecurity/client/Http.java index ac169b9..e304e35 100644 --- a/duo-client/src/main/java/com/duosecurity/client/Http.java +++ b/duo-client/src/main/java/com/duosecurity/client/Http.java @@ -32,6 +32,7 @@ public class Http { public static final int DEFAULT_TIMEOUT_SECS = 60; private static final int RATE_LIMIT_ERROR_CODE = 429; + private static final String CA_BUNDLE_VERSION = "1.0"; public static final String UserAgentString = "Duo API Java/0.8.1-SNAPSHOT"; private final String method; @@ -223,7 +224,8 @@ protected Http(String inMethod, String inHost, String inUri, int timeout) { headers = new Headers.Builder(); headers.add("Host", host); - headers.add("user-agent", UserAgentString); + headers.add("user-agent", String.format("%s ca_bundle/%s (ca_pinning=%s)", + UserAgentString, CA_BUNDLE_VERSION, "enabled")); CertificatePinner pinner = Util.createPinner(host, DEFAULT_CA_CERTS); @@ -397,6 +399,10 @@ public void addHeader(String name, String value) { headers.add(name, value); } + void setHeader(String name, String value) { + headers.set(name, value); + } + public void addParam(String name, String value) { params.put(name, value); } @@ -675,6 +681,9 @@ public T build() { if (caCerts != null) { duoClient.useCustomCertificates(caCerts); } + String caPinningStatus = disableCaPinning ? "disabled" : "enabled"; + duoClient.setHeader("user-agent", String.format("%s ca_bundle/%s (ca_pinning=%s)", + UserAgentString, CA_BUNDLE_VERSION, caPinningStatus)); if (disableCaPinning) { duoClient.disableCaPinning(); } diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java new file mode 100644 index 0000000..5fb81cc --- /dev/null +++ b/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java @@ -0,0 +1,54 @@ +package com.duosecurity.client; + +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import okhttp3.Headers; +import org.junit.Test; + +public class HttpUserAgentTest { + + private String getUserAgent(Http http) throws Exception { + Field headersField = Http.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Headers.Builder headersBuilder = (Headers.Builder) headersField.get(http); + return headersBuilder.build().get("user-agent"); + } + + @Test + public void testDefaultBuilder_includesCaBundleVersion() throws Exception { + Http http = new Http.HttpBuilder("GET", "api-host.duosecurity.com", "/auth/v2/check") + .build(); + + String userAgent = getUserAgent(http); + assertTrue(userAgent.contains("ca_bundle/1.0")); + } + + @Test + public void testDefaultBuilder_includesCaPinningEnabled() throws Exception { + Http http = new Http.HttpBuilder("GET", "api-host.duosecurity.com", "/auth/v2/check") + .build(); + + String userAgent = getUserAgent(http); + assertTrue(userAgent.contains("(ca_pinning=enabled)")); + } + + @Test + public void testDisableCaPinning_includesCaPinningDisabled() throws Exception { + Http http = new Http.HttpBuilder("GET", "api-host.duosecurity.com", "/auth/v2/check") + .disableCaPinning() + .build(); + + String userAgent = getUserAgent(http); + assertTrue(userAgent.contains("(ca_pinning=disabled)")); + } + + @Test + public void testLegacyConstructor_includesCaBundleAndPinningEnabled() throws Exception { + Http http = new Http("GET", "api-host.duosecurity.com", "/auth/v2/check"); + + String userAgent = getUserAgent(http); + assertTrue(userAgent.contains("ca_bundle/1.0")); + assertTrue(userAgent.contains("(ca_pinning=enabled)")); + } +} From 0bda0e6b4a12815e90bf37ac7571f40f588795ed Mon Sep 17 00:00:00 2001 From: Mihir Pradhan Date: Tue, 28 Jul 2026 09:27:15 -0500 Subject: [PATCH 2/2] Overwrite user agent after custom headers to avoid duplicates --- .../java/com/duosecurity/client/Http.java | 6 +++--- .../duosecurity/client/HttpUserAgentTest.java | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/duo-client/src/main/java/com/duosecurity/client/Http.java b/duo-client/src/main/java/com/duosecurity/client/Http.java index e304e35..f1728dc 100644 --- a/duo-client/src/main/java/com/duosecurity/client/Http.java +++ b/duo-client/src/main/java/com/duosecurity/client/Http.java @@ -681,9 +681,6 @@ public T build() { if (caCerts != null) { duoClient.useCustomCertificates(caCerts); } - String caPinningStatus = disableCaPinning ? "disabled" : "enabled"; - duoClient.setHeader("user-agent", String.format("%s ca_bundle/%s (ca_pinning=%s)", - UserAgentString, CA_BUNDLE_VERSION, caPinningStatus)); if (disableCaPinning) { duoClient.disableCaPinning(); } @@ -696,6 +693,9 @@ public T build() { duoClient.addHeader(name, value); } } + String caPinningStatus = disableCaPinning ? "disabled" : "enabled"; + duoClient.setHeader("user-agent", String.format("%s ca_bundle/%s (ca_pinning=%s)", + UserAgentString, CA_BUNDLE_VERSION, caPinningStatus)); return duoClient; } diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java index 5fb81cc..743bbd3 100644 --- a/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java +++ b/duo-client/src/test/java/com/duosecurity/client/HttpUserAgentTest.java @@ -1,5 +1,6 @@ package com.duosecurity.client; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.lang.reflect.Field; @@ -8,11 +9,15 @@ public class HttpUserAgentTest { - private String getUserAgent(Http http) throws Exception { + private Headers getHeaders(Http http) throws Exception { Field headersField = Http.class.getDeclaredField("headers"); headersField.setAccessible(true); Headers.Builder headersBuilder = (Headers.Builder) headersField.get(http); - return headersBuilder.build().get("user-agent"); + return headersBuilder.build(); + } + + private String getUserAgent(Http http) throws Exception { + return getHeaders(http).get("user-agent"); } @Test @@ -51,4 +56,16 @@ public void testLegacyConstructor_includesCaBundleAndPinningEnabled() throws Exc assertTrue(userAgent.contains("ca_bundle/1.0")); assertTrue(userAgent.contains("(ca_pinning=enabled)")); } + + @Test + public void testCustomUserAgentHeader_isReplacedNotDuplicated() throws Exception { + Http http = new Http.HttpBuilder("GET", "api-host.duosecurity.com", "/auth/v2/check") + .addHeader("user-agent", "MyApp/1.0") + .build(); + + Headers headers = getHeaders(http); + assertEquals(1, headers.values("user-agent").size()); + assertEquals(String.format("%s ca_bundle/1.0 (ca_pinning=enabled)", Http.UserAgentString), + headers.get("user-agent")); + } }