From 3bb83c38ec37b00b052043db3eef27e953e0ce8c Mon Sep 17 00:00:00 2001 From: Mihir Pradhan Date: Fri, 17 Jul 2026 10:30:34 -0500 Subject: [PATCH] Add CA bundle version and pinning status to user agent string Co-Authored-By: Claude Opus 4.6 --- .../src/main/java/com/duosecurity/Client.java | 6 +- .../test/java/com/duosecurity/ClientTest.java | 55 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 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 76c63ff..5adf076 100644 --- a/duo-universal-sdk/src/main/java/com/duosecurity/Client.java +++ b/duo-universal-sdk/src/main/java/com/duosecurity/Client.java @@ -40,6 +40,8 @@ public class Client { private static final String USER_AGENT_VERSION = "1.3.3-SNAPSHOT"; + private static final String CA_BUNDLE_VERSION = "1.0"; + // ************************************************** // Fields // ************************************************** @@ -373,7 +375,9 @@ public Client build() throws DuoException { client.apiHost = apiHost; client.redirectUri = redirectUri; client.useDuoCodeAttribute = useDuoCodeAttribute; - client.userAgent = userAgent; + String caPinningStatus = caPinningDisabled ? "disabled" : "enabled"; + client.userAgent = format("%s ca_bundle/%s (ca_pinning=%s)", + userAgent, CA_BUNDLE_VERSION, caPinningStatus); client.duoConnector = new DuoConnector(apiHost, proxyHost, proxyPort, caPinningDisabled ? null : caCerts); diff --git a/duo-universal-sdk/src/test/java/com/duosecurity/ClientTest.java b/duo-universal-sdk/src/test/java/com/duosecurity/ClientTest.java index 9d35dc1..c2fcd23 100644 --- a/duo-universal-sdk/src/test/java/com/duosecurity/ClientTest.java +++ b/duo-universal-sdk/src/test/java/com/duosecurity/ClientTest.java @@ -233,7 +233,60 @@ void custom_useragent() throws DuoException { ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); verify(client.duoConnector).exchangeAuthorizationCodeFor2FAResult(stringCaptor.capture(), anyString(), anyString(), anyString(), anyString(), anyString()); String sentUserAgent = stringCaptor.getValue(); - assertTrue(sentUserAgent.startsWith("duo_universal_java") && sentUserAgent.endsWith(appendedUserAgent)); + assertTrue(sentUserAgent.startsWith("duo_universal_java") && sentUserAgent.contains(appendedUserAgent)); + } + + @Test + void userAgent_includes_ca_bundle_version() throws DuoException { + Client client = new Client.Builder(CLIENT_ID, CLIENT_SECRET, API_HOST, HTTPS_REDIRECT_URI).build(); + client.duoConnector = Mockito.mock(DuoConnector.class); + + try { + client.exchangeAuthorizationCodeFor2FAResult("duo_code", Mockito.mock(TokenValidator.class)); + } catch (Exception e) { + // ignored + } + + ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); + verify(client.duoConnector).exchangeAuthorizationCodeFor2FAResult(stringCaptor.capture(), anyString(), anyString(), anyString(), anyString(), anyString()); + String sentUserAgent = stringCaptor.getValue(); + assertTrue(sentUserAgent.contains("ca_bundle/1.0")); + } + + @Test + void userAgent_includes_ca_pinning_enabled() throws DuoException { + Client client = new Client.Builder(CLIENT_ID, CLIENT_SECRET, API_HOST, HTTPS_REDIRECT_URI).build(); + client.duoConnector = Mockito.mock(DuoConnector.class); + + try { + client.exchangeAuthorizationCodeFor2FAResult("duo_code", Mockito.mock(TokenValidator.class)); + } catch (Exception e) { + // ignored + } + + ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); + verify(client.duoConnector).exchangeAuthorizationCodeFor2FAResult(stringCaptor.capture(), anyString(), anyString(), anyString(), anyString(), anyString()); + String sentUserAgent = stringCaptor.getValue(); + assertTrue(sentUserAgent.contains("(ca_pinning=enabled)")); + } + + @Test + void userAgent_includes_ca_pinning_disabled() throws DuoException { + Client client = new Client.Builder(CLIENT_ID, CLIENT_SECRET, API_HOST, HTTPS_REDIRECT_URI) + .disableCaPinning() + .build(); + client.duoConnector = Mockito.mock(DuoConnector.class); + + try { + client.exchangeAuthorizationCodeFor2FAResult("duo_code", Mockito.mock(TokenValidator.class)); + } catch (Exception e) { + // ignored + } + + ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); + verify(client.duoConnector).exchangeAuthorizationCodeFor2FAResult(stringCaptor.capture(), anyString(), anyString(), anyString(), anyString(), anyString()); + String sentUserAgent = stringCaptor.getValue(); + assertTrue(sentUserAgent.contains("(ca_pinning=disabled)")); } @Test