Skip to content

Encode HTTP Basic credentials as UTF-8, not in the platform default charset - #29694

Merged
davsclaus merged 2 commits into
apache:mainfrom
Dhairya-007:fix/basic-auth-credentials-utf8
Nov 12, 2026
Merged

davsclaus merged 2 commits into
apache:mainfrom
Dhairya-007:fix/basic-auth-credentials-utf8

Conversation

@Dhairya-007

Copy link
Copy Markdown
Contributor

Problem

Five places build an Authorization: Basic (or Proxy-Authorization: Basic) header by Base64-encoding user:password through the no-argument String.getBytes(), which uses the JVM's default charset:

component file
camel-as2 AS2HeaderUtils.addAuthorizationHeader
camel-keycloak KeycloakProducer (token revocation)
camel-keycloak KeycloakTokenIntrospector
camel-smpp SmppConnectionFactory.connectProxy
camel-wordpress WordpressBasicAuthentication
String auth = clientId + ":" + clientSecret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
request.setHeader("Authorization", "Basic " + encodedAuth);

RFC 7617 §2.1 says the credentials are encoded with UTF-8 unless the server names another charset, and that is what Keycloak, WordPress and ordinary HTTP proxies expect.

The JVM default is only UTF-8 when nothing overrides it. -Dfile.encoding=ISO-8859-1, file.encoding=COMPAT, or any JVM before 18 (which follows the platform locale, so US-ASCII under the C/POSIX locale that containers often use) all give something else. A user name or password containing a non-ASCII character then produces a different Authorization header on different machines, and the server answers 401 although the credentials are correct. Because the bytes are Base64-encoded, nothing in the failure points at the encoding — this is the kind of defect that gets debugged for a day.

Fix

Pass StandardCharsets.UTF_8 explicitly in all five places. Credentials made only of ASCII characters encode to exactly the same bytes as before, so nothing changes for the common case.

camel-smpp's connectProxy also wrote the CONNECT request line, the configured proxy headers and the line separators, and read the proxy's response, in the default charset. Those are converted too, so the whole exchange is built one way rather than two.

Tests

AS2HeaderUtilsTest.addAuthorizationHeaderEncodesCredentialsAsUtf8 adds a Basic header for a password with a non-ASCII character and asserts the exact header value against the Base64 of the UTF-8 bytes. It fails on main under any JVM whose default charset is not UTF-8, and pins the encoding from now on.

The other four call sites are the same one-line change inside methods that do network I/O (an HTTP POST to Keycloak, a CXF WebClient configuration, a proxy CONNECT), so they have no seam to assert on without a server; the shared behaviour is what the AS2 test covers.

…harset

Five places build an Authorization/Proxy-Authorization header by
Base64-encoding "user:password" through String.getBytes() with no argument,
which uses the JVM's default charset:

  camel-as2       AS2HeaderUtils.addAuthorizationHeader
  camel-keycloak  KeycloakProducer (token revocation)
  camel-keycloak  KeycloakTokenIntrospector
  camel-smpp      SmppConnectionFactory.connectProxy
  camel-wordpress WordpressBasicAuthentication

RFC 7617 says the credentials are encoded with UTF-8 unless the server asks
for another charset, and that is what Keycloak, WordPress and ordinary HTTP
proxies expect. The default charset is only UTF-8 when nothing overrides it:
-Dfile.encoding, file.encoding=COMPAT, or a JVM before 18 following the
platform locale, all give something else. A user name or password holding a
non-ASCII character then produces a different Authorization header on
different machines, and the server rejects it with 401 even though the
credentials are right. Since the bytes are Base64-encoded, nothing in the
error points at the encoding.

All five now pass StandardCharsets.UTF_8 explicitly. Credentials made only of
ASCII characters encode to exactly the same bytes as before.

camel-smpp's connectProxy also wrote the CONNECT request line, the proxy
headers and the line separators, and read the proxy's response, in the
default charset; those are converted too, so the whole exchange is built
one way.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, a nice tidy fix, and the PR description is very clear about why the failure is so hard to diagnose. I checked the monorepo: these five were the only places that build a Basic / Proxy-Authorization header from the platform default charset (camel-oauth's OAuthTokenRequest and the AS2 proxy auth in AS2ClientConnection already use UTF-8), so the client side is consistent after this. AS2HeaderUtilsTest.addAuthorizationHeaderEncodesCredentialsAsUtf8 pins the exact header value, and the escaped ä keeps the source ASCII-safe.

A few notes, none blocking:

  • [Suggestion, follow-up] The receiving side has the same issue: camel-netty-http's HttpServerChannelHandler.extractBasicAuthSubject decodes the Basic credentials with Charset.defaultCharset() (HttpServerChannelHandler.java:387). Switching it to UTF-8 would be a good small follow-up PR.
  • [Question] For SmppConnectionFactory.connectProxy, switching the CONNECT line, headers and reply reader to UTF-8 looks fine (ASCII in practice). Was it mainly for consistency? HTTP/1.x doesn't really define non-ASCII proxyHeaders values.
  • [Nit] Camel commits and PR titles usually carry a JIRA key (CAMEL-XXXX: ...).

On the upgrade guide: main requires Java 21, where the default charset is already UTF-8, so only deployments that override file.encoding and use non-ASCII credentials see a different header, and they now get the RFC 7617 behaviour. No migration note needed.

Claude Code on behalf of @davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five credential-encoding fixes are correct per RFC 7617 §2, which mandates UTF-8 for the user-id:password string before Base64-encoding in HTTP Basic auth. The fix is well-motivated and consistently applied across all call sites.

One issue to address: the AS2 test is a tautology on the JVMs that Camel CI actually runs.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

Recomputing the expectation the way the code does makes the assertion vacuous
on a JVM whose default charset is already UTF-8, which is every JVM Camel CI
runs. The expected header is now written out, and the ISO-8859-1 encoding of
the same credentials is asserted to differ, so the test says what has to go on
the wire rather than agreeing with whatever the code did.
@Dhairya-007

Copy link
Copy Markdown
Contributor Author

Thanks both, and thanks for sweeping the monorepo for the remaining call sites — good to know camel-oauth and AS2ClientConnection were already right.

The tautology is fixed. @gnodet-bot is right: I computed the expectation the same way the code does, so on Java 21 (default charset UTF-8) the assertion agreed with whatever the code produced. The test now asserts a written-out value and that the ISO-8859-1 encoding of the same credentials differs from it:

assertEquals("Basic dXNlcjpww6Rzc3dvcmQ=", message.getFirstHeader(AS2Header.AUTHORIZATION).getValue());

// what the default charset would have produced instead, had it not been UTF-8
assertNotEquals("Basic " + Base64.getEncoder().encodeToString(("user:" + password).getBytes(ISO_8859_1)),
        message.getFirstHeader(AS2Header.AUTHORIZATION).getValue());

So it now pins the wire format rather than restating the implementation. (dXNlcjpww6Rzc3dvcmQ= is user:pässword in UTF-8; the ISO-8859-1 form is dXNlcjpw5HNzd29yZA==.)

On SmppConnectionFactory.connectProxy — yes, consistency, and you are right that HTTP/1.x does not define non-ASCII values for the proxyHeaders the user configures. My reasoning was that leaving four getBytes() and an InputStreamReader on the default charset two lines away from the one I was fixing would read as an oversight, and that UTF-8 is byte-identical for the ASCII that actually belongs there. Happy to narrow it to just the credentials line if you would rather keep the change minimal.

The receiving side is a good catch — HttpServerChannelHandler.extractBasicAuthSubject decoding with Charset.defaultCharset() has exactly the same failure mode from the other end. I will send that as a separate PR rather than widening this one.

JIRA — I have no credentials for issues.apache.org from this environment, so I cannot create the ticket. If you want a CAMEL-XXXX on this, could a committer open it (or give me the number) and I will retitle and amend?

@Dhairya-007

Copy link
Copy Markdown
Contributor Author

The receiving-side follow-up is open as #29711 — HttpServerChannelHandler.extractBasicAuthSubject now decodes with UTF-8, with a test that asserts Basic dXNlcjpww6Rzc3dvcmQ= comes back as pässword.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second commit addresses the AS2 test tautology precisely.

addAuthorizationHeaderEncodesCredentialsAsUtf8 test (new commit):

  • Uses a fixed expected value "Basic dXNlcjpww6Rzc3dvcmQ=" — this is exactly the correct UTF-8 Base64 of "user:pässword", verified against RFC 7617
  • The assertNotEquals on the ISO-8859-1 encoding (dXNlcjpw5HNzd29yZA==) is meaningful — the two encodings genuinely differ, so the assertion is not vacuous on any JVM
  • Comment explains the intent clearly: "makes the assertion vacuous on a JVM whose default charset is already UTF-8 ... the expected header is now written out"

The original five fixes (AS2, Keycloak×2, SMPP, WordPress) remain correct and are ready to merge. davsclaus has already approved; this is a clean, well-scoped bug fix.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: still good to merge, the new commit resolves the tautology point.

addAuthorizationHeaderEncodesCredentialsAsUtf8 now asserts the written-out Basic dXNlcjpww6Rzc3dvcmQ=, which is the UTF-8 Base64 of user:pässword, so the test pins the wire format instead of restating the implementation. The extra assertNotEquals against the ISO-8859-1 form is redundant after the assertEquals, but it documents intent, so no need to change it. Thanks also for answering the SMPP question and for opening #29711 for the receiving side; the two PRs touch disjoint files.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-as2/camel-as2-api
  • components/camel-keycloak
  • components/camel-smpp
  • components/camel-wordpress

🔁 1 test passed only after a retry on JDK 21 (1 retried attempt)

Recovered flaky tests on JDK 21 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui AiPanelAcpTest.usageUpdateFeedsTheTokenCounter 1 1234 + the 66 this turn added ==> expected: <1300> but was: <1234>

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 13 of 723 tested, 27 compile-only — current: 13 all tested

Maveniverse Scalpel detected 13 affected modules (current approach: 13).

Skip-tests mode would test 13 modules (4 direct + 9 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (13)
  • camel-as2 ← downstream of org.apache.camel:camel-as2-api
  • camel-as2-api ← components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/AS2HeaderUtils.java, components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/AS2HeaderUtilsTest.java
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-jbang-run ← downstream of org.apache.camel:camel-catalog
  • camel-keycloak ← components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/KeycloakProducer.java, components/camel-keycloak/src/main/java/org/apache/camel/component/keycloak/security/KeycloakTokenIntrospector.java
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-smpp ← components/camel-smpp/src/main/java/org/apache/camel/component/smpp/SmppConnectionFactory.java
  • camel-wordpress ← components/camel-wordpress/src/main/java/org/apache/camel/component/wordpress/api/auth/WordpressBasicAuthentication.java
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (40 modules, 5m 39s total)

Total reactor time: 5m 39s

Module Duration Status
Camel :: JBang :: MCP 43.4s SUCCESS
Camel :: YAML DSL :: Validator 27.2s SUCCESS
Camel :: AS2 :: API 24.5s SUCCESS
Camel :: JBang :: Plugin :: Validate 23.1s SUCCESS
Camel :: Component DSL 21.9s SUCCESS
Camel :: YAML DSL 21.7s SUCCESS
Camel :: SMPP 20.7s SUCCESS
Camel :: Catalog :: Camel Catalog 19.5s SUCCESS
Camel :: Wordpress 19.4s SUCCESS
Camel :: JBang :: Run 18.9s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 12.6s SUCCESS
Camel :: Docs 12.0s SUCCESS
Camel :: Kamelet Main 11.9s SUCCESS
Camel :: JBang :: Plugin :: Testing 9.7s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 6.7s SUCCESS
Camel :: YAML DSL :: Deserializers 6.3s SUCCESS
Camel :: Catalog :: Camel Route Parser 6.1s SUCCESS
Camel :: Assembly 4.9s SUCCESS
Camel :: JBang :: Plugin :: Generate 4.4s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 4.3s SUCCESS
Camel :: All Components Sync point 4.2s SUCCESS
Camel :: Catalog :: Maven 2.8s SUCCESS
Camel :: YAML DSL :: Maven Plugins 2.0s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 1.7s SUCCESS
Camel :: JBang :: Integration tests 1.6s SUCCESS
Camel :: JBang :: Plugin :: MCP 1.5s SUCCESS
Camel :: Coverage 1.1s SUCCESS
Camel :: Catalog :: Dummy Component 1.0s SUCCESS
Camel :: Launcher :: Container 0.8s SUCCESS
Camel :: JBang :: Plugin :: Edit 0.8s SUCCESS
Camel :: Catalog :: Console 0.7s SUCCESS
Camel :: Endpoint DSL :: Support 0.7s SUCCESS
Camel :: JBang :: Main 0.7s SUCCESS
Camel :: AS2 :: Component n/a
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a
Camel :: Keycloak n/a
Camel :: Launcher n/a

Top 20 slowest modules:

  • Camel :: JBang :: MCP (43.4s)
  • Camel :: YAML DSL :: Validator (27.2s)
  • Camel :: AS2 :: API (24.5s)
  • Camel :: JBang :: Plugin :: Validate (23.1s)
  • Camel :: Component DSL (21.9s)
  • Camel :: YAML DSL (21.7s)
  • Camel :: SMPP (20.7s)
  • Camel :: Catalog :: Camel Catalog (19.5s)
  • Camel :: Wordpress (19.4s)
  • Camel :: JBang :: Run (18.9s)
  • Camel :: JBang :: Plugin :: Kubernetes (12.6s)
  • Camel :: Docs (12.0s)
  • Camel :: Kamelet Main (11.9s)
  • Camel :: JBang :: Plugin :: Testing (9.7s)
  • Camel :: Catalog :: Camel Report Maven Plugin (6.7s)
  • Camel :: YAML DSL :: Deserializers (6.3s)
  • Camel :: Catalog :: Camel Route Parser (6.1s)
  • Camel :: Assembly (4.9s)
  • Camel :: JBang :: Plugin :: Generate (4.4s)
  • Camel :: YAML DSL :: Validator Maven Plugin (4.3s)

⚙️ View full build and test results

@oscerd oscerd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the producer side. Every one of the five spots now Base64-encodes user:password from its UTF-8 bytes, which is what RFC 7617 asks for and what browsers and Camel's own consumers expect:

  • camel-as2 AS2HeaderUtils.addAuthorizationHeader
  • camel-keycloak KeycloakProducer.revokeToken and KeycloakTokenIntrospector.introspect
  • camel-smpp SmppConnectionFactory.connectProxy (the CONNECT line, the proxy headers and the response reader are pinned to UTF-8 too, so the whole exchange is charset-stable, not just the credential)
  • camel-wordpress WordpressBasicAuthentication

ASCII credentials are unchanged, so only a non-ASCII user name or password moves, and it moves to the correct bytes. The AS2 test is written well: it asserts the literal Basic dXNlcjpww6Rzc3dvcmQ= that has to go on the wire for user:pässword instead of recomputing it the way the code does, and the assertNotEquals against the ISO-8859-1 encoding pins down that the charset is exactly what changed.

Two things to consider, neither blocking:

  1. This is the encode half. The matching decode fix for the netty-http server is #29711, and the two agree on the same dXNlcjpww6Rzc3dvcmQ= vector, which is good to see.
  2. For a deployment on Java 17 whose default charset is not UTF-8 (Windows-1252, say) and which uses non-ASCII Basic credentials, the Authorization header value on the wire changes. That is the bug being fixed, but it is also a visible behavior change, so a line in the 4.24 upgrade guide under changed header values would help anyone in that narrow case. On Java 18 and later the default charset is already UTF-8 (JEP 400), so most deployments see nothing change.

One pointer, unrelated to this PR but in the same family: camel-workday AuthClientForIntegration.createPostMethod builds its Basic header with Arrays.toString(Base64.getEncoder().encode(...)), which emits [66, 97, ...] rather than a Base64 string. That is a separate and more serious defect sitting on top of the same getBytes() charset issue, so it is better fixed on its own rather than folded in here.

Claude Code on behalf of oscerd

@davsclaus davsclaus added this to the 4.24.0 milestone Nov 12, 2026
@davsclaus davsclaus added the bug Something isn't working label Nov 12, 2026
@davsclaus
davsclaus merged commit 8e479d7 into apache:main Nov 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants