Skip to content

feat: add Bearer token authentication support#227

Merged
w1am merged 8 commits into
masterfrom
w1am/dev-1643-add-bearer-token-authentication-support-to-kurrentdb-rust
May 4, 2026
Merged

feat: add Bearer token authentication support#227
w1am merged 8 commits into
masterfrom
w1am/dev-1643-add-bearer-token-authentication-support-to-kurrentdb-rust

Conversation

@w1am
Copy link
Copy Markdown

@w1am w1am commented May 4, 2026

Adds Bearer token authentication to the Rust client. Until now requests could only carry HTTP Basic credentials. This PR adds a parallel path so callers can send Authorization: Bearer <token> for OAuth/OIDC flows. The bridge work in DEV-1644 depends on it.

The change is fully additive: nothing existing breaks, no migrations needed.

What's new

  • Authentication enum with Basic(Credentials) and Bearer(Bytes) variants.
  • Authentication::basic(login, password) and Authentication::bearer(token) constructors.
  • impl From<Credentials> for Authentication so the existing API surface relaxes without breaking callers.
  • build_request_metadata and http_configure_auth now match on the variant and emit the right Authorization header.

How to use it

use kurrentdb::{Authentication, AppendToStreamOptions};

let opts = AppendToStreamOptions::default()
    .authenticated(Authentication::bearer("eyJ..."));

If you're already using Credentials::new(...), nothing changes:

options.authenticated(Credentials::new("admin", "changeit")); // still works

Backward compatibility

Note

No existing code needs to change. Credentials, its constructor, connection-string parsing, and the TOML/JSON serde format for credentials are all unchanged.

.authenticated() was relaxed from (Credentials) to (impl Into<Authentication>). Calls passing a Credentials value continue to compile because of the new From impl. Per cargo's semver rules this counts as a minor change.


Closes DEV-1643.
Required by DEV-1644.

@linear
Copy link
Copy Markdown

linear Bot commented May 4, 2026

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add Bearer token authentication support to KurrentDB Rust client

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add Authentication enum supporting Basic and Bearer token variants
• Implement Bearer token authentication for OAuth/OIDC integration
• Maintain backward compatibility with existing Credentials API
• Update HTTP and gRPC request metadata to handle both auth types
• Add comprehensive unit tests for auth header construction
Diagram
flowchart LR
  A["Authentication Enum<br/>Basic/Bearer"] --> B["build_authorization_header<br/>gRPC metadata"]
  A --> C["http_configure_auth<br/>HTTP requests"]
  D["Credentials"] --> A
  E[".authenticated() builder"] --> A
  B --> F["Authorization header<br/>Basic/Bearer format"]
  C --> F
Loading

Grey Divider

File Changes

1. kurrentdb/src/types.rs ✨ Enhancement +40/-0

Add Authentication enum with Basic and Bearer variants

• Introduce Authentication enum with Basic(Credentials) and Bearer(Bytes) variants
• Add Authentication::basic() and Authentication::bearer() constructor methods
• Implement From<Credentials> for Authentication for backward compatibility

kurrentdb/src/types.rs


2. kurrentdb/src/request.rs ✨ Enhancement +143/-15

Update gRPC metadata to support Bearer authentication

• Refactor build_request_metadata() to handle Authentication enum instead of just Credentials
• Extract build_authorization_header() function to generate correct header format per auth type
• Add 9 comprehensive unit tests covering Basic/Bearer headers, special characters, conversion, and
 builder acceptance

kurrentdb/src/request.rs


3. kurrentdb/src/http/mod.rs ✨ Enhancement +22/-6

Update HTTP auth configuration for Bearer support

• Add resolve_authentication() helper to handle per-operation override and connection-string
 fallback
• Update http_configure_auth() to branch on Authentication variant and use basic_auth() or
 bearer_auth()
• Change parameter from Option<&Credentials> to Option<&Authentication>

kurrentdb/src/http/mod.rs


View more (5)
4. kurrentdb/src/http/persistent_subscriptions.rs ✨ Enhancement +10/-40

Refactor persistent subscriptions auth handling

• Replace inline auth resolution with calls to resolve_authentication() helper
• Update 5 call sites to use new helper function for cleaner code
• Maintain same authentication fallback logic (per-call override then connection-string default)

kurrentdb/src/http/persistent_subscriptions.rs


5. kurrentdb/src/operations/gossip.rs ✨ Enhancement +6/-1

Update gossip operation for Authentication enum

• Convert default user credentials to Authentication::Basic before passing to
 http_configure_auth()
• Update gossip HTTP read operation to work with new authentication type

kurrentdb/src/operations/gossip.rs


6. kurrentdb/src/options/mod.rs ✨ Enhancement +2/-2

Update CommonOperationOptions to use Authentication

• Replace credentials: Option<Credentials> with authentication: Option<Authentication> in
 CommonOperationOptions
• Update import from Credentials to Authentication

kurrentdb/src/options/mod.rs


7. eventstore-macros/src/lib.rs ✨ Enhancement +9/-3

Update authenticated builder macro for generic auth

• Update .authenticated() builder method to accept impl Into<Authentication> instead of just
 Credentials
• Update documentation to mention Bearer token support
• Store authentication in common_operation_options.authentication field

eventstore-macros/src/lib.rs


8. kurrentdb/Cargo.toml ⚙️ Configuration changes +1/-1

Bump version to 1.1.0

• Bump version from 1.0.0 to 1.1.0 (additive minor release)

kurrentdb/Cargo.toml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 4, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Bearer metadata can panic🐞 Bug ☼ Reliability
Description
build_authorization_header formats Bearer tokens verbatim and then calls
MetadataValue::try_from(...).expect(...), which will panic if the resulting header contains
non-ASCII or other invalid metadata characters.
Code

kurrentdb/src/request.rs[R44-65]

+fn build_authorization_header(
+    auth: &Authentication,
+) -> tonic::metadata::MetadataValue<tonic::metadata::Ascii> {
+    use tonic::metadata::MetadataValue;
+
+    let header = match auth {
+        Authentication::Basic(Credentials { login, password }) => {
+            let login = String::from_utf8_lossy(login);
+            let password = String::from_utf8_lossy(password);
+            let encoded = base64::engine::general_purpose::STANDARD
+                .encode(format!("{}:{}", login, password));
+            format!("Basic {}", encoded)
+        }
+        Authentication::Bearer(token) => {
+            let token = String::from_utf8_lossy(token);
+            format!("Bearer {}", token)
+        }
+    };
+
+    MetadataValue::try_from(header.as_str())
+        .expect("Auth header value should be valid metadata header value")
+}
Evidence
The gRPC path builds a MetadataValue<Ascii> for the authorization header and uses expect(...) on
the conversion result. For Bearer, the token is inserted verbatim (after a lossy UTF-8 conversion),
so any non-ASCII characters (including U+FFFD produced by lossy conversion) or other invalid header
characters will cause try_from to return Err and the client will panic while constructing
request metadata.

kurrentdb/src/request.rs[44-65]
kurrentdb/src/types.rs[81-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`build_authorization_header` uses `MetadataValue::try_from(...).expect(...)`. With Bearer tokens being inserted verbatim, invalid header characters (notably any non-ASCII) will cause a panic when building gRPC request metadata.

### Issue Context
Bearer tokens are not base64-encoded by this client; they are sent as provided. Even if a caller passes valid UTF-8, non-ASCII characters are enough to make `MetadataValue<Ascii>` construction fail. Additionally, `String::from_utf8_lossy` can introduce U+FFFD, which is non-ASCII.

### Fix Focus Areas
- kurrentdb/src/request.rs[44-65]

### Suggested fix
- Replace `expect(...)` with non-panicking handling:
 - Option A (preferred): change `build_request_metadata` / `build_authorization_header` to return `crate::Result<MetadataMap>` and propagate an error up through `commands::new_request`.
 - Option B: return `Option<MetadataValue<Ascii>>` from `build_authorization_header` and only insert the header when conversion succeeds, while emitting a structured error log (without logging the token).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unchecked UTF-8 in HTTP auth🐞 Bug ⛨ Security
Description
http_configure_auth uses std::str::from_utf8_unchecked on Credentials and Bearer token
Bytes, but Credentials::new / Authentication::bearer allow constructing these from arbitrary
bytes; invalid UTF-8 violates the unsafe precondition and can lead to undefined behavior.
Code

kurrentdb/src/http/mod.rs[R22-29]

+    match auth_opt {
+        Some(crate::Authentication::Basic(creds)) => builder.basic_auth(
            unsafe { std::str::from_utf8_unchecked(creds.login.as_ref()) },
            unsafe { Some(std::str::from_utf8_unchecked(creds.password.as_ref())) },
-        )
-    } else {
-        builder
+        ),
+        Some(crate::Authentication::Bearer(token)) => {
+            builder.bearer_auth(unsafe { std::str::from_utf8_unchecked(token.as_ref()) })
+        }
Evidence
http_configure_auth converts raw Bytes to &str with from_utf8_unchecked, which is only sound
if the bytes are valid UTF-8. The public constructors accept S: Into<Bytes> with no UTF-8
validation, so callers can provide non-UTF8 data and trigger UB when building HTTP requests.

kurrentdb/src/http/mod.rs[18-31]
kurrentdb/src/types.rs[43-54]
kurrentdb/src/types.rs[72-87]
Best Practice: Rust std::str::from_utf8_unchecked

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`http_configure_auth` uses `std::str::from_utf8_unchecked` on authentication bytes. Since `Credentials` and `Authentication::Bearer` can be constructed from arbitrary `Bytes`, this can invoke undefined behavior when bytes are not valid UTF-8.

### Issue Context
This is a public-facing API footgun: users can pass `Vec<u8>`, `Bytes`, or other non-UTF8 sources into `Credentials::new` / `Authentication::bearer`.

### Fix Focus Areas
- kurrentdb/src/http/mod.rs[18-31]
- kurrentdb/src/types.rs[43-54]
- kurrentdb/src/types.rs[72-87]

### Suggested fix
- Remove `from_utf8_unchecked` and use a safe conversion:
 - Prefer `std::str::from_utf8(...)` and handle `Err` (log + skip auth, or plumb an error), or
 - Use `String::from_utf8_lossy(...)` and pass the resulting `Cow<str>` into `basic_auth` / `bearer_auth` (both accept `Display`).
- Optionally: tighten the API by storing auth material as `String` (or a validated wrapper) instead of `Bytes`, if acceptable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread kurrentdb/src/http/mod.rs
Comment thread kurrentdb/src/request.rs
w1am added 7 commits May 4, 2026 15:34
The bridge work in DEV-1644 needs Bearer auth on the wire, but the Rust
client only knows how to send Basic. This adds an Authentication enum
alongside the existing Credentials struct, with Basic and Bearer variants,
so per-call options can carry either flavor.

The migration is intentionally non-breaking. Credentials and its
constructor are unchanged, From<Credentials> for Authentication is
provided, and .authenticated() now takes impl Into<Authentication> so
existing callsites keep compiling without changes.

Bumps the crate to 1.1.0.

Closes DEV-1643.
http_configure_auth was reading login/password/token bytes through
from_utf8_unchecked, but Credentials::new and Authentication::bearer
both accept arbitrary Into<Bytes>, so a perfectly safe caller could
hand in non-UTF-8 and trigger UB.

Swapped to from_utf8_lossy, which is what request.rs already does for
the gRPC path. reqwest's basic_auth and bearer_auth take impl Display,
and Cow<str> implements Display, so it slots in directly.
build_authorization_header used .expect() on MetadataValue::try_from.
For Bearer that's risky: the token goes onto the wire verbatim, and
HTTP/2 rejects control chars (NUL, LF, CR, others below 0x20, DEL),
so a token someone read from a file or env var without trimming the
trailing newline would crash the client.

Now the function returns Option<MetadataValue<Ascii>>. On Err it emits
a tracing::warn! tagged with the auth kind (basic/bearer, never the
token itself) and returns None. build_request_metadata skips the
authorization header when None, the server replies AccessDenied, and
the warn log gives operators a clear hint about what went wrong.

Worth flagging: tonic's MetadataValue<Ascii> is more permissive than
the name implies. It accepts bytes 0x80 through 0xFF, full UTF-8, and
U+FFFD. What actually breaks is the control-char set above; the new
tests cover that.
A pass of small cleanups after running the diff through code review.

Authentication grew a kind() helper so the tracing labels aren't typed
out as raw "basic"/"bearer" strings each time. The nested if-let in
build_request_metadata flattens to a single .as_deref().and_then(...)
chain. The three near-duplicate "bearer with invalid char" tests
collapse into one parameterized loop. settings_from moves to the top
of the test module so it isn't buried mid-file.

Comment-wise: trimmed narrative explanations down to the WHY, and
added a brief note on why http_configure_auth uses from_utf8_lossy.
CI bumped to 1.95 and three new lints fired on pre-existing code, none
of it from this PR. Silenced dead_code on the generated common.rs
module (unused proto types are noise from prost-build), collapsed the
nested if-let in the connection-id selector with a &&-let, and rewrote
two `% 2 == 0` checks in the random node tiebreak as
.is_multiple_of(2).
CI's `cargo fmt --check` caught a few line-wraps in request.rs that I
missed locally.
The auth code had accumulated a lot of comments that just restated the
type or function name. Stripped the Basic variant doc, the basic/bearer
constructor docs, and the resolve_authentication docstring. Removed the
narrative explanation of the from_utf8_lossy choice; the change reads
clearly without it. Trimmed the panic-prevention comment to one line
and tightened the .authenticated() rustdoc and the parameterized
test's rationale comment.

Net is -11 lines of comments. Kept only the WHYs that aren't obvious
from the code: the From<Credentials> migration path on the enum, "sent
verbatim" on the Bearer variant, base64 hints in the tests, the
security intent of not logging tokens, and the panic-vector context.
@w1am w1am force-pushed the w1am/dev-1643-add-bearer-token-authentication-support-to-kurrentdb-rust branch from 1da8ffd to a4fa77c Compare May 4, 2026 11:35
Add unit tests for http_configure_auth and resolve_authentication, the
HTTP auth surface used by gossip and persistent subscriptions, which had
no coverage. Drop three gRPC tests whose behavior is already exercised
elsewhere (From<Credentials> via the .authenticated() builder, the bearer
builder path via the override test, and the private builder's invalid-char
handling via the public metadata test).
@w1am w1am merged commit a16c686 into master May 4, 2026
74 of 78 checks passed
@w1am w1am deleted the w1am/dev-1643-add-bearer-token-authentication-support-to-kurrentdb-rust branch May 4, 2026 12:27
w1am added a commit that referenced this pull request May 4, 2026
The options! macro was changed in #227 (credentials -> authentication)
without bumping eventstore-macros, so cargo publish verification of
kurrentdb pulled the old 0.0.1 from crates.io and failed to compile.
w1am added a commit that referenced this pull request May 4, 2026
The options! macro was changed in #227 (credentials -> authentication)
without bumping eventstore-macros, so cargo publish verification of
kurrentdb pulled the old 0.0.1 from crates.io and failed to compile.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant