Skip to content

Commit c2d5f11

Browse files
feat(auth): standardize OAuth2 Basic authentication across SDK
- Update addAuthHeaders() to use OAuth2 Basic auth format - Uses Authorization: Basic base64(clientId:clientSecret) header - Remove non-standard X-Client-ID and X-Client-Secret headers - X-License-Key retained as fallback for backward compatibility - Add comprehensive tests for OAuth2 Basic auth - Update CHANGELOG with authentication changes
1 parent fa9aa89 commit c2d5f11

3 files changed

Lines changed: 96 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Requires enterprise portal authentication
1616
- **PRRecord.closedAt**: Added optional `closedAt` field to track when a PR was closed
1717

18+
### Changed
19+
20+
- **OAuth2 Basic Authentication**: Standardized authentication to use industry-standard OAuth2 Basic auth
21+
- Uses `Authorization: Basic base64(clientId:clientSecret)` header
22+
- Removed non-standard `X-Client-ID` and `X-Client-Secret` headers
23+
- `X-License-Key` retained for backward compatibility (fallback when OAuth2 credentials not provided)
24+
- Matches TypeScript SDK authentication pattern for consistency across all SDKs
25+
1826
### Fixed
1927

2028
- **getPlanStatus endpoint**: Fixed endpoint path from `/api/v1/orchestrator/plan/{id}` to `/api/v1/plan/{id}` to match agent proxy routes

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
import java.io.Closeable;
3737
import java.io.IOException;
3838
import java.net.URI;
39+
import java.nio.charset.StandardCharsets;
3940
import java.util.ArrayList;
41+
import java.util.Base64;
4042
import java.util.Collections;
4143
import java.util.HashMap;
4244
import java.util.List;
@@ -1665,16 +1667,20 @@ private void addAuthHeaders(Request.Builder builder) {
16651667
return;
16661668
}
16671669

1668-
// Prefer license key
1669-
if (config.getLicenseKey() != null && !config.getLicenseKey().isEmpty()) {
1670-
builder.header("X-License-Key", config.getLicenseKey());
1670+
// OAuth2-style: Authorization: Basic base64(clientId:clientSecret)
1671+
if (config.getClientId() != null && !config.getClientId().isEmpty() &&
1672+
config.getClientSecret() != null && !config.getClientSecret().isEmpty()) {
1673+
String credentials = config.getClientId() + ":" + config.getClientSecret();
1674+
String encoded = Base64.getEncoder().encodeToString(
1675+
credentials.getBytes(StandardCharsets.UTF_8)
1676+
);
1677+
builder.header("Authorization", "Basic " + encoded);
16711678
return;
16721679
}
16731680

1674-
// Fall back to client credentials
1675-
if (config.getClientId() != null && config.getClientSecret() != null) {
1676-
builder.header("X-Client-ID", config.getClientId());
1677-
builder.header("X-Client-Secret", config.getClientSecret());
1681+
// Fallback: X-License-Key header (backward compatibility)
1682+
if (config.getLicenseKey() != null && !config.getLicenseKey().isEmpty()) {
1683+
builder.header("X-License-Key", config.getLicenseKey());
16781684
}
16791685
}
16801686

src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ void shouldNotSendAuthHeadersWithoutCredentials(WireMockRuntimeInfo wmRuntimeInf
448448
// Verify no auth headers were sent when no credentials configured
449449
verify(postRequestedFor(urlEqualTo("/api/request"))
450450
.withoutHeader("X-License-Key")
451-
.withoutHeader("X-Client-Secret"));
451+
.withoutHeader("Authorization"));
452452

453453
System.out.println("✅ Auth headers not sent in community mode (no credentials)");
454454
}
@@ -484,5 +484,79 @@ void shouldSendAuthHeadersWithCredentials(WireMockRuntimeInfo wmRuntimeInfo) {
484484

485485
System.out.println("✅ Auth headers sent when credentials are configured");
486486
}
487+
488+
@Test
489+
@DisplayName("should send OAuth2 Basic auth with clientId and clientSecret")
490+
void shouldSendOAuth2BasicAuth(WireMockRuntimeInfo wmRuntimeInfo) {
491+
stubFor(post(urlEqualTo("/api/request"))
492+
.willReturn(aResponse()
493+
.withStatus(200)
494+
.withHeader("Content-Type", "application/json")
495+
.withBody("{"
496+
+ "\"success\": true,"
497+
+ "\"data\": {\"answer\": \"test\"}"
498+
+ "}")));
499+
500+
// With OAuth2 credentials
501+
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
502+
.agentUrl(wmRuntimeInfo.getHttpBaseUrl())
503+
.clientId("my-client")
504+
.clientSecret("my-secret")
505+
.build());
506+
507+
client.executeQuery(
508+
ClientRequest.builder()
509+
.userToken("")
510+
.query("Test query")
511+
.build()
512+
);
513+
514+
// Verify OAuth2 Basic auth header is sent
515+
String expectedBasic = "Basic " + java.util.Base64.getEncoder().encodeToString(
516+
"my-client:my-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8)
517+
);
518+
verify(postRequestedFor(urlEqualTo("/api/request"))
519+
.withHeader("Authorization", equalTo(expectedBasic)));
520+
521+
System.out.println("✅ OAuth2 Basic auth header sent correctly");
522+
}
523+
524+
@Test
525+
@DisplayName("OAuth2 should take priority over license key")
526+
void shouldPrioritizeOAuth2OverLicenseKey(WireMockRuntimeInfo wmRuntimeInfo) {
527+
stubFor(post(urlEqualTo("/api/request"))
528+
.willReturn(aResponse()
529+
.withStatus(200)
530+
.withHeader("Content-Type", "application/json")
531+
.withBody("{"
532+
+ "\"success\": true,"
533+
+ "\"data\": {\"answer\": \"test\"}"
534+
+ "}")));
535+
536+
// With BOTH OAuth2 credentials AND license key
537+
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
538+
.agentUrl(wmRuntimeInfo.getHttpBaseUrl())
539+
.clientId("my-client")
540+
.clientSecret("my-secret")
541+
.licenseKey("should-be-ignored")
542+
.build());
543+
544+
client.executeQuery(
545+
ClientRequest.builder()
546+
.userToken("")
547+
.query("Test query")
548+
.build()
549+
);
550+
551+
// OAuth2 should take priority
552+
String expectedBasic = "Basic " + java.util.Base64.getEncoder().encodeToString(
553+
"my-client:my-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8)
554+
);
555+
verify(postRequestedFor(urlEqualTo("/api/request"))
556+
.withHeader("Authorization", equalTo(expectedBasic))
557+
.withoutHeader("X-License-Key"));
558+
559+
System.out.println("✅ OAuth2 correctly takes priority over license key");
560+
}
487561
}
488562
}

0 commit comments

Comments
 (0)