Repository navigation
fix(server): decode Basic auth as UTF-8 and split on the first colon - #3290
arshilkxwork wants to merge 7 commits into
Conversation
- Decode the Basic credential as UTF-8 (RFC 7617 section 2.1). US-ASCII
turned every non-ASCII byte into U+FFFD, so a password such as
"ädminpass1" never matched the stored hash and the request got 401.
- Split at the first colon. RFC 7617 bars a colon from the user-id only,
and split(":") answered 400 for a password such as "new:pass1234".
- A credential with no colon, an empty user-id or an empty password
still gets 400.
- Add a LoginApiTest case covering both passwords.
- CypherAPI.toUserPass still split the decoded credential on every colon and returned null for anything but two parts, so a password with ':' passed AuthenticationFilter and then failed on the cypher endpoint. It now splits once, like the filter and Gremlin's handler. - Reword the filter comment: RFC 7617 allows UTF-8 as the only declared charset and does not define a default. - Point the Helm wrapper comment at apache#3284; the TODO it named is gone. - Parse the created user with TypeReference like the other tests in LoginApiTest, and say why the Basic header is built by hand.
imbajin
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The new REST regression covers UTF-8 and colon passwords, but the separate Cypher parser change lacks an automated colon-password case. Evidence: Exact-head diff and test-file review.
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The filter now decodes Basic credentials as UTF-8 and splits on the first colon, and the new LoginApiTest covers both cases on the REST path; the only gap is that the separate Cypher parser change has no automated colon-password case. Evidence: exact-head diff of AuthenticationFilter, CypherAPI and LoginApiTest at 56a4e06; WsAndHttpBasicAuthHandler already uses split(":", 2); no other Basic-auth decoder in hugegraph-server; latest-head workflows are waiting for maintainer approval (action_required), so CI has not run.
- CypherApiTest#testBasicAuthWithColonPassword creates a user whose
password is "a:b:c", grants its group READ and EXECUTE on the graph,
and sends a hand-built UTF-8 Basic header to the Cypher endpoint. It
expects a successful query result, and 401 for a wrong password.
- The user, group, target, belong and accesses it creates are deleted
in reverse order when the test ends.
- With the old split(":") in CypherAPI.toUserPass the test fails with
500 "The 'user-password-pair' can't be null".
- Write the non-ASCII password in LoginApiTest as a literal; checkstyle
flags Unicode escapes.
…f8-colon # Conflicts: # hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: The filter change is correct and the REST path now accepts UTF-8 and colon passwords, but CypherAPI.toUserPass still decodes the header with the URL-safe Base64 decoder, so the Cypher endpoint answers 500 for any credential whose Base64 contains + or /, which is a large share of the non-ASCII passwords this PR enables. Evidence: exact-head build of 577f4c3 on a RocksDB server configured as in run-api-test.sh; LoginApiTest, UserApiTest and CypherApiTest pass (33 tests); curl probes with hand-built UTF-8 Basic headers gave 200 on REST and 500 on Cypher for probe:密码pass1 and probe:a:b:c~; the workflows at this head are waiting for maintainer approval, so CI has not run, and the new tests were not run on HStore.
|
Supplemental review at The REST UTF-8/first-colon fix is focused, and the added Cypher regression is useful. Its credential does not exercise the standard Base64
Please use the standard Base64 decoder and add Cypher endpoint regressions for both cases, including wrong-password rejection. This complements the existing inline discussion, whose reviewer reports REST 200 versus Cypher 500. Shared parsing is optional; no broader refactor is required. Evidence boundary: I inspected the current source paths and verified these encodings; I did not rerun repository tests or live probes. The author's colon-password regression report and the reviewer's runtime probes remain their reported evidence. All five workflows at this exact head—License Checker, Server Memory CI, CodeQL, HugeGraph-Server CI and Server Compatibility CI—currently have |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3290 +/- ##
=============================================
+ Coverage 41.38% 51.75% +10.37%
+ Complexity 6945 2638 -4307
=============================================
Files 762 762
Lines 66934 67399 +465
Branches 8988 9138 +150
=============================================
+ Hits 27698 34881 +7183
+ Misses 36062 28991 -7071
- Partials 3174 3527 +353 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- CypherAPI used Base64.getUrlDecoder(), which rejects '+' and '/'. A Basic header carries standard Base64 (RFC 7617, RFC 4648 section 4), so a credential that encodes to either character got 500 from the Cypher endpoint while REST accepted it. Use Base64.getDecoder(), as WsAndHttpBasicAuthHandler does. - testBasicAuthWithColonPassword now runs a:b:c, a:b:c~ and a:b:c? for cypher_colon_user. The last two encode with '+' and '/', which the test asserts so a rename cannot drop that coverage unnoticed. Each password must return the query result, and a wrong one 401. With the URL-safe decoder back in place the test fails with 500 "The 'user-password-pair' can't be null". - Skip that test outside standalone mode. Its group, target and access setup uses the standalone auth API; on HStore groups are scoped to a graphspace and creating the access gets 403 "group belongs to another graphspace", which failed the hstore api-test job. The header parsing it covers does not depend on the backend.
|
Thank you for the review. Commit 7e048d1 switches The same commit addresses the Local results at 7e048d1: the full api-test suite passes on RocksDB (182 tests, 14 skipped) and on HStore with PD and Store started by |
imbajin
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: UTF-8 Basic credentials are decoded on the REST and Cypher paths, but the expected charset is not advertised to clients. Evidence: exact-head 401 handling and RFC 7617 section 2.1.
| if (values.length != 2) { | ||
| // RFC 7617 section 2.1 allows only UTF-8 as the declared charset, and it bars a | ||
| // colon from the user-id alone, so split on the first one and keep the rest | ||
| auth = new String(DatatypeConverter.parseBase64Binary(auth), StandardCharsets.UTF_8); |
There was a problem hiding this comment.
charset="UTF-8"; the PR also notes that Jersey and OkHttp clients still send ISO-8859-1. Those clients cannot infer the new encoding and continue returning 401 for non-ASCII passwords. Please advertise the expected charset in the Basic WWW-Authenticate challenge for both entry points, add a response-level regression, and document any remaining client limitations. Evidence: RFC 7617 section 2.1 defines the challenge charset parameter as the signal for the expected encoding; the exact-head AuthenticationFilter and CypherAPI 401 paths do not include it.
There was a problem hiding this comment.
Thank you for the suggestion. I agree that advertising the expected charset is the signal RFC 7617 section 2.1 defines, and I would like to confirm the intended scope before implementing it.
At present the 401 responses do not carry a Basic challenge. The WWW-Authenticate header contains the error text instead: on a local build of 7e048d1 it is Missing authentication credentials when no credentials are sent and Incorrect username or password when they are wrong, on both the REST and the Cypher paths. Advertising the charset therefore means replacing that value with a standard challenge such as Basic realm="...", charset="UTF-8".
Browsers respond to a Basic challenge by showing their native login dialog, so this would also change what browser-based clients such as Hubble display when credentials are missing or a session has expired.
Would you prefer this change in this PR, or in a separate PR together with the client-side encoding work for Jersey and OkHttp? I can implement it in either place, with a response-level regression for both entry points and a note on the remaining client limitations. If it belongs here, please also let me know which realm value you would like to use.
|
The Server Compatibility CI failure at 7e048d1 is in |
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The filter and the Cypher endpoint now decode Basic credentials as UTF-8 with the standard Base64 alphabet and split on the first colon, which matches WsAndHttpBasicAuthHandler, so all three Server parsers agree on charset, Base64 alphabet and split rule; the only new point is a Helm README pointer to the TODO this PR deletes. Evidence: exact-head diff at 7e048d1 and the surrounding code of AuthenticationFilter.authenticate, CypherAPI.toUserPass and WsAndHttpBasicAuthHandler.parseBasicCredentials; git grep for every Basic-credential parser under hugegraph-server, hugegraph-pd and hugegraph-store; CI at this head passes server_rocksdb, pd_store / hstore, server_hbase and helm / lint-and-render, and the one failing job (build-server-macos-rocksdb (macos-15-intel)) fails in MultiNodeShortestPathApiTest.testPost with BackendException: Interrupted, maybe it is timed out or uses too much memory, outside the code changed here. No tests were rerun locally.
The paragraph on the admin password guard cited the decoding-site TODO in apache#3260, which this branch removes from AuthenticationFilter. Refer to apache#3284 for the Server side, and keep the reference to the encode_prop_value TODO, which is still in docker-entrypoint.sh.
| private void deleteAuth(Deque<String[]> created) { | ||
| while (!created.isEmpty()) { | ||
| String[] entry = created.pop(); | ||
| client().delete(entry[0], entry[1]); |
There was a problem hiding this comment.
🧹 Blocking: no. Summary: This helper discards each DELETE response, so a failed final cleanup can leave fixed-name auth records behind while the test succeeds. Evidence: RestClient.delete() returns a Response, and BaseApiTest.clearData() clears only graph/schema data. Please assert each deletion's expected status while continuing the remaining cleanup, so auth cleanup failures fail the test.
There was a problem hiding this comment.
Thank you. In c9339f0, deleteAuth attempts every deletion and then fails the test if any of them did not return 204, listing the affected records. A cleanup failure is therefore no longer silent, and the remaining records are still removed. testBasicAuthWithNonAsciiOrColonPassword in LoginApiTest now asserts 204 on its user deletion for the same reason.
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: At this head the REST filter, the Cypher endpoint and WsAndHttpBasicAuthHandler all decode Basic credentials as UTF-8 with the standard Base64 alphabet and split on the first colon, and the Helm README note from the last round is fixed; one point not raised earlier is that the 400 paths and the two documented compatibility changes have no automated test. Evidence: exact-head diff 0f86e2a..cd10ba8 (6 files) and the surrounding code of AuthenticationFilter.authenticate, CypherAPI.toUserPass and WsAndHttpBasicAuthHandler.parseBasicCredentials; 7e048d1..cd10ba8 changes only helm/hugegraph/README.md; git grep at the head for how hugegraph-test sends Basic credentials (HttpAuthenticationFeature in BaseApiTest.RestClient plus the two hand-built headers); old and new split rules run side by side on JDK 17.0.20.1. CI at this head: ApiTestSuite passed on HBase (182 run, 13 skipped) and on HStore (176 run, 52 skipped). server_rocksdb (Java 17) is red in the Run api test step before any test starts: the jacocoagent.jar download fails with curl: (28) Failed to connect to github.com port 443, so the API tests did not run on RocksDB at this head and that job needs a re-run; affected-module-tests is red only because it reports that job. No tests were rerun locally.
| // colon from the user-id alone, so split on the first one and keep the rest | ||
| auth = new String(DatatypeConverter.parseBase64Binary(auth), StandardCharsets.UTF_8); | ||
| int colon = auth.indexOf(':'); | ||
| if (colon < 0) { |
There was a problem hiding this comment.
Minor: the 400 paths in this block and the two behaviour changes listed under Compatibility have no automated test. Every API test authenticates through Jersey's HttpAuthenticationFeature with a non-empty user and password, and the two hand-built headers (LoginApiTest.basicAuthGet, CypherApiTest.basicAuthCypher) are well-formed as well, so nothing sends a credential without a colon, with an empty field, or with a trailing colon. Codecov reports one partially covered line for this file in the patch. The description checks admin, admin: and :pa by hand with curl, and nothing checks user:pass: or user::.
Requested change: next to testBasicAuthWithNonAsciiOrColonPassword, send hand-built headers for admin, admin: and :pa and expect 400, and for one of the users created there send <name>:<password>: and <name>:: and expect 401.
There was a problem hiding this comment.
Thank you. c9339f0 adds LoginApiTest#testBasicAuthWithMalformedCredential, which sends hand-built headers. After a control request with test:test that returns 200, it expects 400 for admin, admin: and :pa, and 401 for test:test: and test::, using the user the class creates in setup(). The two trailing-colon cases correspond to the compatibility notes in the description.
- LoginApiTest#testBasicAuthWithMalformedCredential sends hand-built headers for "admin", "admin:" and ":pa" and expects 400, then sends "test:test:" and "test::" for the user created in setup() and expects 401, after a control request with "test:test" that returns 200. This covers the 400 paths and the two trailing-colon changes described in the PR. - CypherApiTest#deleteAuth checks each DELETE and, after attempting all of them, fails the test if any auth record was left behind. testBasicAuthWithNonAsciiOrColonPassword asserts 204 on its user deletion for the same reason.
Purpose of the PR
AuthenticationFilterdecoded the HTTP Basic credential as US-ASCII and split it on every colon. The user API accepts passwords that contain non-ASCII characters or a colon, so an account given such a password, including the admin account, could no longer authenticate with Basic auth. A non-ASCII password received 401, and a password containing a colon received 400.Main Changes
AuthenticationFilternow decodes the credential as UTF-8 (RFC 7617, section 2.1) and splits it at the first colon, treating the remainder as the password. A credential without a colon, or with an empty user-id or password, still receives 400. The GrizzlyCharsetsimport is replaced withStandardCharsets, and the TODO that referred to this issue is removed.CypherAPI.toUserPassparses the header separately for the Cypher endpoint. It split the credential on every colon and decoded it withBase64.getUrlDecoder(), which rejects the+and/characters of standard Base64. Such credentials, which are common with non-ASCII passwords, passed the filter and then received 500 from the Cypher endpoint. It now usesBase64.getDecoder()andsplit(":", 2), consistent withWsAndHttpBasicAuthHandler.The comment in the Helm Server wrapper (
server-deployment.yaml) and the corresponding note in the chart README referred to the removed TODO and now refer to #3284.Compatibility
user:pass:previously authenticated withpassand is now evaluated aspass:;user::now receives 401 instead of 400.HttpAuthenticationFeatureand the OkHttpCredentials.basicdefault used byOkHttpBasicAuthInterceptorin hugegraph-commons. Changes to those clients are outside the scope of this PR.Out of scope
In line with the discussion on the issue:
values.schema.jsonand the Server wrapper) is retained, because the chart deploys published images that do not include this fix. Its removal also depends on a separate entrypoint fix for spaces.Verifying these changes
LoginApiTest#testBasicAuthWithNonAsciiOrColonPasswordcreates users with the passwordsädminpass1andnew:pass1234and callsGET graphspaces/DEFAULT/graphswith a UTF-8 Basic header. It expects 200 for the correct password and 401 for an incorrect one.CypherApiTest#testBasicAuthWithColonPasswordgrantscypher_colon_userREAD and EXECUTE on the graph through a group and queries the Cypher endpoint with the passwordsa:b:c,a:b:c~anda:b:c?. The latter two produce headers containing+and/. Each password must return the query result, and an incorrect password must receive 401. All entities created by the test are deleted afterwards, and a failed deletion fails the test. The setup relies on the standalone auth API, so the test is skipped on HStore.run-api-test.sh(183 tests, 14 skipped) and on HStore with PD and Store started byinstall-backend.sh(177 tests, 52 skipped). Checkstyle andeditorconfig:checkreport no issues in the changed files.ädminpass1, and the Cypher test fails with 500 (The 'user-password-pair' can't be null) with either the formersplit(":")or the URL-safe decoder.RocksDBSnapshotRestoreTestthat fail on macOS because/varresolves to/private/var. This PR does not modify that code.LoginApiTest#testBasicAuthWithMalformedCredentialsends hand-built headers. After a control request withtest:testthat returns 200, it expects 400 foradmin,admin:and:pa, and 401 fortest:test:andtest::, which covers the trailing-colon changes listed under Compatibility.Does this PR potentially affect the following parts?
:, and non-ASCII passwords sent as UTF-8Documentation Status
Doc - TODO: required documentation is pending; complete it before merging.Doc - Done: documentation is included here or linked below.Doc - No Need: no user-visible documentation is affected.The ASCII-decoding text in the Helm chart (
values.schema.jsonand the README validation section) describes the published image that the chart deploys. It should be updated together with the guard once a release includes this fix.