Skip to content

feat(resp): add AUTH command with --require-pass server password - #17

Merged
Saxy merged 2 commits into
Saxy:mainfrom
ozykhan:feat/resp-auth
Jul 29, 2026
Merged

feat(resp): add AUTH command with --require-pass server password#17
Saxy merged 2 commits into
Saxy:mainfrom
ozykhan:feat/resp-auth

Conversation

@ozykhan

@ozykhan ozykhan commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Description

Implements the Redis-compatible AUTH command for the RESP listener, with a new --require-pass / TSD_REQUIRE_PASS server password option.

  • AUTH <password> (single-password mode, Redis < 6.0 compatible) and AUTH <username> <password> (ACL form — only the implicit default user exists until API Key System with Per-Key ACLs #9 lands)
  • Replies +OK on success, -ERR invalid password on failure, per the Redis AUTH spec
  • Unauthenticated connections may only issue AUTH, PING, and QUIT; everything else gets -NOAUTH Authentication required
  • When no password is configured, AUTH is a +OK no-op and every connection starts authenticated — fully backward compatible
  • Failed AUTH attempts are logged at warn level with the remote address, mirroring the existing malformed-frame log

Component: Networking/RESP, CLI

Type of Change:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Performance optimization (no change in behavior, improved speed/memory)
  • Refactoring (no functional changes, code cleanup)
  • Build / CI / Documentation

Related Issue

Closes #7


Technical Deep Dive & Context

The implementation follows the touch points laid out in the issue:

  • Password hashing off the hot path. The password is hashed once at startup with bcrypt.GenerateFromPassword (stored as []byte on Server); bcrypt.CompareHashAndPassword runs only inside the AUTH handler. GET/SET/DEL never touch bcrypt. bcrypt's cost factor also acts as a natural brute-force rate limit on AUTH attempts. No new dependency — golang.org/x/crypto is already a direct dependency (TLS, ChaCha20).
  • Per-connection auth state. connState gains authenticated bool (plus remoteAddr, captured once in OnOpen so the failed-AUTH log path allocates nothing per attempt). State initializes to true when no password is configured, false otherwise. Works identically on the plaintext and TLS traffic paths since both funnel through dispatch.
  • Zero-overhead guarantee. With --require-pass unset the per-command cost is one EqualFold(cmd, "AUTH") length-check-first comparison and a single bool test that short-circuits. Dispatch benchmarks confirm no measurable change and no new allocations (table below).
  • Guard placement. AUTH is checked before the auth guard so unauthenticated clients can always authenticate; the guard then allow-lists PING/QUIT per Redis semantics before the command switch.
  • dispatch signature changed from dispatch(args, out) to dispatch(st, args, out) to carry connection state; benchmarks updated accordingly.
  • Trade-off: the two-arg ACL form rejects any username other than default (byte-equal, case-sensitive like Redis) with the same -ERR invalid password reply, so usernames aren't oracle-able. A real ACL system is deferred to API Key System with Per-Key ACLs #9.

Performance & Benchmarks (If Applicable)

Workload: go test -bench BenchmarkDispatch -benchmem -benchtime 3000000x -count 3 (Apple M4 Pro, best of 3)

Metric Before After Delta
DispatchGetHit 15.43 ns/op, 0 allocs 15.03 ns/op, 0 allocs noise
DispatchSet 26.30 ns/op, 1 alloc 26.07 ns/op, 1 alloc noise
DispatchSetParallel 119.3 ns/op, 1 alloc 117.4 ns/op, 1 alloc noise

No measurable change; zero new allocations on the hot path. (The 1 alloc on Set is the pre-existing fakeStore key copy in the benchmark harness, unchanged.)

BenchmarkDispatchGetHit-12          3000000    15.03 ns/op    0 B/op    0 allocs/op
BenchmarkDispatchSet-12             3000000    26.07 ns/op   16 B/op    1 allocs/op
BenchmarkDispatchSetParallel-12     3000000   117.4 ns/op    16 B/op    1 allocs/op

How Has This Been Tested?

Written test-first (tests were red before the implementation existed):

  • TestRESPServer_AuthRequired — NOAUTH before auth, PING allowed pre-auth, wrong password rejected, correct password unlocks the connection, and auth state is per-connection (a second connection must re-authenticate)
  • TestRESPServer_AuthWithUsernameAUTH default <pass> accepted, unknown username rejected
  • TestRESPServer_AuthNoPasswordConfigured — no-op +OK path, commands work without auth
  • TestRESPServer_AuthWrongArity — arity errors for 1 and 4 args
  • Config tests for the --require-pass flag, TSD_REQUIRE_PASS env var, and empty default

go test ./..., go test -race ./..., and go vet ./... all pass locally with zero warnings.


Checklist

  • My code follows the existing code style of this project
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally (go test ./... and go test -race ./...)
  • I have updated the documentation (README, comments, or any relevant docs)
  • My changes generate no new go vet warnings
  • Any breaking changes are documented and communicated

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added optional password protection for RESP connections.
    • Configure via --require-pass or TSD_REQUIRE_PASS; when enabled, clients must authenticate with AUTH before other commands.
    • Supports AUTH <password> and AUTH default <password>, with appropriate handling for wrong credentials, wrong argument counts, and username-based AUTH.
    • PING and QUIT remain available before authentication.
  • Bug Fixes
    • Authentication enforcement is now consistent for plaintext and TLS connections.
  • Tests
    • Added unit tests for defaults, CLI/env configuration, and authentication/session behavior.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ozykhan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3fb43ec4-bf69-43de-b211-4724ea142afc

📥 Commits

Reviewing files that changed from the base of the PR and between e6c78c2 and 13c51ab.

📒 Files selected for processing (7)
  • config/config.go
  • config/config_test.go
  • internal/resp/benchmark_test.go
  • internal/resp/server.go
  • internal/resp/server_test.go
  • internal/shard/runner.go
  • server/server.go
📝 Walkthrough

Walkthrough

Changes

RESP authentication

Layer / File(s) Summary
Password configuration
config/config.go, config/config_test.go, server/server.go
Adds --require-pass and TSD_REQUIRE_PASS, exposes the configured password, tests loading behavior, and passes it into RESP server construction.
Connection authentication flow
internal/resp/server.go, internal/shard/runner.go
Adds bcrypt-backed AUTH handling, per-connection authentication state, unauthenticated command restrictions, TLS/plaintext dispatch integration, and the AUTH command constant.
Authentication behavior tests
internal/resp/server_test.go
Tests required authentication, credential failures, username-based AUTH, connection state isolation, disabled authentication, and argument validation.
Dispatch compatibility updates
internal/resp/benchmark_test.go
Updates dispatch benchmarks to provide authenticated connection state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RESPServer
  participant connState
  participant bcrypt
  Client->>RESPServer: AUTH password
  RESPServer->>connState: Check authentication state
  RESPServer->>bcrypt: Verify password hash
  bcrypt-->>RESPServer: Verification result
  RESPServer->>connState: Mark connection authenticated
  RESPServer-->>Client: +OK or authentication error
Loading

Suggested reviewers: saxy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: Redis RESP AUTH support with the --require-pass option.
Description check ✅ Passed The PR follows the template and fills all required sections, including component, issue link, implementation details, testing, and checklist.
Linked Issues check ✅ Passed The changes implement the requested AUTH semantics, password config, per-connection auth state, NOAUTH gating, and warning logs for #7.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; tests, benchmarks, config, and RESP server updates all support the AUTH feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/resp/server_test.go (1)

109-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Engine.Stop instead of deprecated gnet.Stop.

golangci-lint flags this call as SA1019 deprecated; gnet.Stop only shuts down the last-registered Engine for the address/protocol and can leak Engines when WithReuseAddr/WithReusePort are enabled elsewhere. This mirrors an existing pattern already in the file, so no new risk is introduced here, but since the linter flags this specific new call, using the gnet.Engine returned from OnBoot would be more future-proof.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resp/server_test.go` around lines 109 - 120, Update startServer to
capture the gnet.Engine returned through the server’s OnBoot callback, then call
that engine’s Stop method during cleanup instead of the deprecated gnet.Stop
function. Preserve the existing timeout context and cleanup behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/resp/server.go`:
- Around line 331-342: Handle QUIT explicitly in the switch within
Server.dispatch, returning a successful Redis response instead of falling
through to the unknown-command default. Preserve its existing pre-auth allowance
and limit this change to the reply; do not add connection-closing behavior.

---

Nitpick comments:
In `@internal/resp/server_test.go`:
- Around line 109-120: Update startServer to capture the gnet.Engine returned
through the server’s OnBoot callback, then call that engine’s Stop method during
cleanup instead of the deprecated gnet.Stop function. Preserve the existing
timeout context and cleanup behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eee48b22-9c97-43a6-a07a-59f7bdfe8c88

📥 Commits

Reviewing files that changed from the base of the PR and between 474d721 and 82dbf1d.

📒 Files selected for processing (7)
  • config/config.go
  • config/config_test.go
  • internal/resp/benchmark_test.go
  • internal/resp/server.go
  • internal/resp/server_test.go
  • internal/shard/runner.go
  • server/server.go

Comment thread internal/resp/server.go
Implements Redis-compatible AUTH for the RESP listener:

- AUTH <password> and AUTH <username> <password> (only the implicit
  "default" user exists until ACLs land)
- New --require-pass flag / TSD_REQUIRE_PASS env var; the password is
  bcrypt-hashed once at startup and compared only on AUTH, never on
  the hot path
- Unauthenticated connections may only issue AUTH, PING, and QUIT;
  everything else returns -NOAUTH Authentication required
- When no password is configured, AUTH replies +OK as a no-op and all
  connections start authenticated (zero overhead, backward compatible)
- Failed AUTH attempts are logged at warn level with the remote address

Closes Saxy#7

Signed-off-by: Faruk Can Özkan <ozkanfarukcan@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/resp/server_test.go (3)

126-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unchecked Close() errors flagged by errcheck.

Static analysis flags the unchecked return of conn.Close()/conn2.Close() at these three new sites.

🔧 Proposed fix
-	defer conn.Close()
+	defer func() { _ = conn.Close() }()

(apply analogously to conn2.Close() at Line 147)

Also applies to: 147-147, 156-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resp/server_test.go` at line 126, Handle the return values from
conn.Close() and conn2.Close() in the affected test cleanup sites, including the
close at the later third location, so errcheck no longer reports unchecked
errors. Use the existing test error-handling convention rather than silently
discarding the results.

Source: Linters/SAST tools


55-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Old pipeline test duplicates the new startServer/expectReply helpers.

TestRESPServer_GetSetPingPipeline still rolls its own inline expect closure and manual go ListenAndServe/defer gnet.Stop teardown (Lines 58-63, 68-81), duplicating the logic just extracted into startServer/expectReply. Worth converging on the shared helpers now that they exist.

♻️ Suggested refactor
 func TestRESPServer_GetSetPingPipeline(t *testing.T) {
-	addr := freeAddr(t)
-	srv := NewServer(addr, newFakeStore(), nil, log.NewNoOpLogger(), nil, "")
-	go func() { _ = srv.ListenAndServe() }()
-	defer func() {
-		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
-		defer cancel()
-		_ = gnet.Stop(ctx, "tcp://"+addr)
-	}()
-
+	addr := startServer(t, "")
 	conn := dialWithRetry(t, addr)
 	defer conn.Close()
-
-	expect := func(name, send, want string) {
-		t.Helper()
-		if _, err := conn.Write([]byte(send)); err != nil {
-			t.Fatalf("%s write: %v", name, err)
-		}
-		got := make([]byte, len(want))
-		_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
-		if _, err := io.ReadFull(conn, got); err != nil {
-			t.Fatalf("%s read: %v", name, err)
-		}
-		if string(got) != want {
-			t.Fatalf("%s: got %q want %q", name, got, want)
-		}
-	}
+	expect := func(name, send, want string) { expectReply(t, conn, name, send, want) }

Also applies to: 109-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resp/server_test.go` around lines 55 - 91, Refactor
TestRESPServer_GetSetPingPipeline to use the shared startServer setup and
expectReply assertion helpers instead of its inline ListenAndServe/gnet.Stop
lifecycle and expect closure. Preserve all existing PING, SET, GET hit/miss, and
pipelined reply assertions and ordering.

109-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the server’s Shutdown path instead of gnet.Stop in tests.

Server already captures the gnet.Engine in OnBoot and defines Shutdown(ctx) to call Engine.Stop(ctx), while gnet.Stop is deprecated as a global shutdown API. Have the test helpers call srv.Shutdown instead of adding another global shutdown point, even though the current ListenAndServe options don’t trigger the WithReuseAddr/WithReusePort leak.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/resp/server_test.go` around lines 109 - 120, Update the startServer
cleanup to call the server instance’s Shutdown method with the existing timeout
context instead of gnet.Stop, using srv as the shutdown owner and preserving the
current cleanup timing and error handling.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/resp/server_test.go`:
- Around line 122-150: The TestRESPServer_AuthRequired coverage should also
verify unauthenticated QUIT handling. Add a QUIT request on an unauthenticated
connection and assert the documented Redis-style teardown response and
connection closure, using the existing dispatch/expectReply test helpers without
altering authentication coverage.

---

Nitpick comments:
In `@internal/resp/server_test.go`:
- Line 126: Handle the return values from conn.Close() and conn2.Close() in the
affected test cleanup sites, including the close at the later third location, so
errcheck no longer reports unchecked errors. Use the existing test
error-handling convention rather than silently discarding the results.
- Around line 55-91: Refactor TestRESPServer_GetSetPingPipeline to use the
shared startServer setup and expectReply assertion helpers instead of its inline
ListenAndServe/gnet.Stop lifecycle and expect closure. Preserve all existing
PING, SET, GET hit/miss, and pipelined reply assertions and ordering.
- Around line 109-120: Update the startServer cleanup to call the server
instance’s Shutdown method with the existing timeout context instead of
gnet.Stop, using srv as the shutdown owner and preserving the current cleanup
timing and error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bfcd9b65-ceba-44a7-96bf-3b5d641ea5b7

📥 Commits

Reviewing files that changed from the base of the PR and between 82dbf1d and e6c78c2.

📒 Files selected for processing (7)
  • config/config.go
  • config/config_test.go
  • internal/resp/benchmark_test.go
  • internal/resp/server.go
  • internal/resp/server_test.go
  • internal/shard/runner.go
  • server/server.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/shard/runner.go
  • config/config.go
  • internal/resp/benchmark_test.go
  • config/config_test.go
  • server/server.go
  • internal/resp/server.go

Comment thread internal/resp/server_test.go
ozykhan added a commit to ozykhan/Tellstone that referenced this pull request Jul 29, 2026
QUIT was allow-listed for unauthenticated connections but had no
dispatch case, so it fell through to the unknown-command error.
Implement it per Redis semantics: reply +OK, stop parsing pipelined
commands, flush pending replies, and close the connection.

Also replace the deprecated gnet.Stop with Server.Shutdown in the
auth test helper.

Addresses review feedback on Saxy#17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Faruk Can Özkan <ozkanfarukcan@gmail.com>
@Saxy
Saxy self-requested a review July 29, 2026 13:08

@Saxy Saxy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you for your Contribution!

please avoid including "Co-authored-by: Claude" having AI generated/co-authored tags creates legal and licensing gray areas.

Comment thread internal/resp/server_test.go Outdated
QUIT was allow-listed for unauthenticated connections but had no
dispatch case, so it fell through to the unknown-command error.
Implement it per Redis semantics: reply +OK, stop parsing pipelined
commands, flush pending replies, and close the connection.

Also replace the deprecated gnet.Stop with Server.Shutdown in the
auth test helper.

Addresses review feedback on Saxy#17.

Signed-off-by: Faruk Can Özkan <ozkanfarukcan@gmail.com>
@ozykhan

ozykhan commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Understood — removed the Co-authored-by trailers from both commits (history rewritten and force-pushed); DCO sign-offs are unchanged. Also applied the SetReadDeadline error-check suggestion in the QUIT test.

@ozykhan
ozykhan requested a review from Saxy July 29, 2026 17:28
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.

AUTH command for RESP protocol

2 participants