Skip to content

feat(java): dashboard session auth, CSRF, RBAC#387

Merged
pratyush618 merged 2 commits into
masterfrom
feat/java-dashboard-auth
Jul 7, 2026
Merged

feat(java): dashboard session auth, CSRF, RBAC#387
pratyush618 merged 2 commits into
masterfrom
feat/java-dashboard-auth

Conversation

@pratyush618

@pratyush618 pratyush618 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Brings the Java SDK dashboard to auth parity with the other SDKs. This is the first of a stacked series that ports the full dashboard + auth/OAuth surface (ops endpoints, native reads, workflows, OAuth, Spring, webhooks) — each subsequent PR stacks on this one.

What this adds

  • Session authentication — users + sessions persisted in the settings KV (no new tables, backend-agnostic). PBKDF2-HMAC-SHA256 (600k iterations) with a constant-time verify and a dummy-hash timing defense against user enumeration.
  • CSRF — double-submit cookie bound to the session token, enforced on every state-changing method (login/setup exempt).
  • RBACadmin / viewer. All writes are admin-only except self-service (logout, change-password); all reads are allowed for any authenticated user.
  • First-run setup — a setup_required 503 gate until the first admin is created via POST /api/auth/setup; env bootstrap via TASKITO_DASHBOARD_ADMIN_USER / TASKITO_DASHBOARD_ADMIN_PASSWORD.
  • Cookiestaskito_session (HttpOnly) + taskito_csrf, SameSite=Strict; --insecure-cookies drops Secure for local HTTP.
  • Legacy shared-token mode — passing a token keeps the previous behaviour (fixed admin identity, no users/sessions), now also accepting Authorization: Bearer and X-Taskito-Token.

The monolithic DashboardServer is restructured into layered, acyclic subpackages — support (http/json/errors), routing (route table), store (settings KV seam), auth, api — behind a route table.

Behaviour change

The no-token default now requires session auth (first-run setup) instead of serving the API open. Existing --token deployments are unaffected.

Tests

36 dashboard tests: PBKDF2 golden vector (verified against an externally-produced 600k hash), auth store, all auth endpoints, RBAC, CSRF, setup gate, cookies. Checkstyle + Spotless clean.

Summary by CodeRabbit

  • New Features
    • Added session-based dashboard sign-in flow: setup, login, logout, “who am I”, and password change.
    • Improved dashboard cookie and CSRF protection for authenticated write actions.
    • Expanded dashboard API routing for stats/jobs/queues/dead letters/metrics/workers plus admin actions (pause/resume/cancel/retry).
    • Added --insecure-cookies option for local dashboard use.
  • Bug Fixes
    • Improved dashboard response reliability with safer error handling when requests fail or clients disconnect.

KV-backed users/sessions (PBKDF2 600k, byte-compatible with the other SDKs), double-submit CSRF, admin/viewer RBAC, first-run setup and env-admin bootstrap, plus a legacy shared-token fallback. Restructures the dashboard into layered support/routing/store/auth/api packages behind a route table. The no-token default now requires auth instead of serving open.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces the dashboard's single-token authentication with a session/CSRF-based auth system supporting RBAC, PBKDF2 password hashing, and OAuth user provisioning, alongside a legacy shared-token fallback mode. It introduces new auth, routing, API-handler, and support packages, refactors DashboardServer around a Router, and adds corresponding tests.

Changes

Dashboard auth and routing rework

Layer / File(s) Summary
Support utilities
sdks/java/src/main/java/org/byteveda/taskito/dashboard/support/DashboardError.java, Http.java, Json.java, package-info.java
New DashboardError exception with HTTP-status factories, Http for query/body/JSON response handling, and Json codec helpers using Jackson.
Auth models and crypto helpers
sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/User.java, Session.java, Tokens.java, Cookies.java, PasswordHasher.java, package-info.java, sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/PasswordHasherTest.java
User/Session records, Tokens for random token generation, Cookies for Set-Cookie construction/parsing, and PasswordHasher implementing PBKDF2-SHA256 hashing/verification with OAuth sentinel handling and tests.
AuthStore persistence
sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java, sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/AuthStoreTest.java
New AuthStore for KV-backed user CRUD, authentication, OAuth provisioning/role bootstrap, session lifecycle (create/resolve/prune), and env-driven admin bootstrap, with tests.
Authorization policy and request context
sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Policy.java, RequestContext.java
Policy enforces setup/auth/CSRF/RBAC rules for /api/* requests; RequestContext resolves session/CSRF state and validates double-submit CSRF.
Session handlers and legacy token auth
sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthHandlers.java, TokenAuth.java
AuthHandlers implements status/setup/login/logout/whoami/changePassword; TokenAuth implements legacy shared-token extraction/matching and open-mode responses.
Routing infrastructure
sdks/java/src/main/java/org/byteveda/taskito/dashboard/routing/Handler.java, Req.java, Router.java, package-info.java
New Handler interface, Req request container, and Router with fluent route registration and regex-based dispatch.
Core API handlers and contract wiring
sdks/java/src/main/java/org/byteveda/taskito/dashboard/api/CoreHandlers.java, Contract.java, package-info.java, sdks/java/src/main/java/org/byteveda/taskito/dashboard/store/SettingsAccess.java, package-info.java
CoreHandlers exposes stats/jobs/dead-letter/metrics/worker/action endpoints; Contract moved to dashboard.api package; SettingsAccess interface/adapter added for settings persistence.
DashboardServer wiring and CLI flag
sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java, sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
DashboardServer refactored to build a Router backed by AuthStore/AuthHandlers/TokenAuth, adds a secureCookies start() overload; CLI adds --insecure-cookies option.
Integration tests and test client
sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardAuthTest.java, DashboardClient.java, DashboardTest.java
New end-to-end auth/RBAC/CSRF tests, a DashboardClient test helper for cookie/CSRF-aware requests and seeding, and an updated existing contract test using the new client.

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

Possibly related PRs

  • ByteVeda/taskito#318: Both PRs modify DashboardServer's startup/start(...) overloads and static-asset handling during server initialization.
  • ByteVeda/taskito#385: Both PRs touch dashboard cookie/auth wiring by propagating an insecureCookies/secureCookies toggle into server startup and session cookie handling.

Suggested labels: tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding Java dashboard session auth with CSRF protection and RBAC.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/java-dashboard-auth

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: 7

🧹 Nitpick comments (1)
sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardClient.java (1)

82-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded password creates implicit test coupling.

seedUser always creates the user with "password123". DashboardAuthTest.changePasswordRotatesCredential relies on this literal as old_password, so a future edit to this default would silently break that test with a confusing failure far from the root cause.

♻️ Suggested overload to make the coupling explicit
     static Session seedUser(Taskito queue, String username, String role) {
+        return seedUser(queue, username, role, "password123");
+    }
+
+    static Session seedUser(Taskito queue, String username, String role, String password) {
         AuthStore store = store(queue);
-        store.createUser(username, "password123", role);
+        store.createUser(username, password, role);
         return store.createSession(username, role);
     }
🤖 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 `@sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardClient.java`
around lines 82 - 90, The hardcoded default password in DashboardClient.seedUser
creates hidden coupling with tests that assume the same literal, so make the
dependency explicit by introducing a shared constant or an overload that accepts
the initial password and use that from seedAdmin/seedUser. Update
DashboardAuthTest.changePasswordRotatesCredential to reference the shared
default instead of an inline literal, so future password changes only need to
happen in one place.
🤖 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 `@sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java`:
- Around line 64-85: AuthStore.createUser() still performs a non-atomic
read-modify-write on auth:users, so concurrent calls can overwrite each other
and lose a newly created user. Update the user-creation flow to use an atomic
write path in AuthStore, centering the fix around createUser(), rawUsers(), and
saveUsers() so the existence check and insert happen under a single
lock/CAS-style update. Also review setup() and countUsers() to ensure they do
not rely on a precheck that can race with createUser().
- Around line 106-117: updatePassword in AuthStore only changes password_hash,
so existing sessions remain valid after a password change. After saving the new
password, also revoke all sessions for the affected username using the existing
session/auth session management helpers in this module, and make sure the caller
flow re-issues a fresh session for the current user if needed. Use the
updatePassword method and related AuthStore session handling code to locate the
fix.

In `@sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Cookies.java`:
- Around line 63-65: The legacy auth cookie path is hardcoding the Secure
attribute to false, so secure-cookie configuration is not being honored. Update
Cookies.legacyTokenCookie to accept a secureCookies flag and pass it into format
instead of always using false, then thread that flag through
TokenAuth.openCookie so callers can propagate the dashboard’s secureCookies
setting when issuing the legacy token cookie.

In
`@sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/PasswordHasher.java`:
- Around line 45-79: Update PasswordHasher.verify so it performs the same dummy
PBKDF2 work before returning false for null or OAUTH_PREFIX values, instead of
exiting immediately. Reuse the existing dummyVerify/password derivation pattern
(or an equivalent call into verify/pbkdf2) so OAuth-only and unknown-user cases
have comparable cost, while keeping the final result false.

In `@sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java`:
- Around line 121-128: The generic exception handler in
DashboardServer.handleRequest is returning RuntimeException/IOException messages
directly to clients, which can leak internal details. Change the catch block to
send a generic 500 response body for unexpected errors instead of using
e.getMessage(), and keep any specific details only in server-side logging if
needed. Use the handleRequest method and the safeRespond call in DashboardServer
to locate the fix.
- Around line 111-120: The dispatch routing in DashboardServer is missing
explicit handling for /readiness and /metrics, so they currently fall through to
serveStatic() instead of returning their intended responses. Update
dispatch(HttpExchange exchange) to route these paths before the static fallback,
alongside /health and /api/*, and ensure the corresponding handlers are invoked
from DashboardServer rather than relying on the SPA fallback. If these endpoints
are not meant to be publicly exposed, remove them from Policy.PUBLIC_PATHS
instead.

In
`@sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/AuthStoreTest.java`:
- Around line 154-162: The test changePasswordInvalidatesOldCredential in
AuthStoreTest only checks that the old password stops working, but it does not
verify session revocation after AuthStore.updatePassword. Create a session
before calling updatePassword, then assert that the previously created session
can no longer be resolved afterward while the new password still authenticates
successfully; use the existing AuthStore, createUser, updatePassword,
authenticate, and getSession paths to lock in the session invalidation behavior.

---

Nitpick comments:
In `@sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardClient.java`:
- Around line 82-90: The hardcoded default password in DashboardClient.seedUser
creates hidden coupling with tests that assume the same literal, so make the
dependency explicit by introducing a shared constant or an overload that accepts
the initial password and use that from seedAdmin/seedUser. Update
DashboardAuthTest.changePasswordRotatesCredential to reference the shared
default instead of an inline literal, so future password changes only need to
happen in one place.
🪄 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

Run ID: 3604b6ca-3e40-455d-9177-e01748a24573

📥 Commits

Reviewing files that changed from the base of the PR and between 78a5078 and 9f96b6b.

📒 Files selected for processing (31)
  • sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/api/Contract.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/api/CoreHandlers.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/api/package-info.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthHandlers.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Cookies.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/PasswordHasher.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Policy.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/RequestContext.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Session.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/TokenAuth.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Tokens.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/User.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/package-info.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/routing/Handler.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/routing/Req.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/routing/Router.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/routing/package-info.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/store/SettingsAccess.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/store/package-info.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/support/DashboardError.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/support/Http.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/support/Json.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/support/package-info.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardAuthTest.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardClient.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/AuthStoreTest.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/PasswordHasherTest.java

Comment thread sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java Outdated
Comment thread sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java Outdated
Comment thread sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Cookies.java Outdated
@pratyush618 pratyush618 self-assigned this Jul 7, 2026
Addresses PR review: revoke a user's sessions on password change; return a generic error code (not raw exception text) on 500 and log the detail; pay a dummy PBKDF2 derivation for OAuth/unknown hashes to equalise timing; thread the secure-cookie flag through the legacy token cookie; synchronize auth:users mutations against in-process lost updates; drop the unimplemented /readiness+/metrics from the public-path set.
@pratyush618

Copy link
Copy Markdown
Collaborator Author

Addressed all review comments in e18f57a:

  • Session revocationupdatePassword now revokes the user's existing sessions (with a regression test).
  • Error masking — the 500 path returns internal_error and logs the detail server-side, instead of echoing raw exception text.
  • Login timingverify pays a dummy PBKDF2 derivation for OAuth/unknown hashes so they can't be distinguished by response time.
  • Secure cookie — the legacy token cookie now honours the secureCookies flag.
  • Atomic writesauth:users mutations are synchronized against in-process lost updates (cross-process CAS isn't available in the KV store; noted).
  • Public paths — dropped the not-yet-implemented /readiness + /metrics from Policy.PUBLIC_PATHS (added with their handlers in the stacked ops PR).

Resolving the threads.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java (1)

91-101: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Executor is never shut down on close() and is unbounded.

Executors.newCachedThreadPool() is created here but only assigned to the server; close() (Line 109) calls server.stop(0), which does not terminate a caller-supplied executor. Its (non-daemon) threads linger and the ExecutorService is never shut down, so repeated create/close cycles (e.g. across tests) leak threads. The cached pool is also unbounded, which allows thread exhaustion under a request burst.

Store the executor as a field and shut it down in close(), and consider a bounded pool.

🔒️ Proposed fix
     private final HttpServer server;
     private final Taskito queue;
+    private final ExecutorService executor;
         HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
-        DashboardServer dashboard = new DashboardServer(server, queue, dir, secureCookies, token);
+        ExecutorService executor = Executors.newCachedThreadPool();
+        DashboardServer dashboard = new DashboardServer(server, queue, dir, secureCookies, token, executor);
         // Seed an env admin before serving so no request races the open setup endpoint.
         if (token == null) {
             dashboard.authStore.bootstrapAdminFromEnv();
         }
         server.createContext("/", dashboard::dispatch);
-        server.setExecutor(Executors.newCachedThreadPool());
+        server.setExecutor(executor);
         server.start();
     `@Override`
     public void close() {
         server.stop(0);
+        executor.shutdownNow();
     }

(constructor updated to accept and assign the executor field.)

🤖 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 `@sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java`
around lines 91 - 101, The DashboardServer startup path creates an
ExecutorService with Executors.newCachedThreadPool() and hands it to HttpServer,
but close() only stops the server and never shuts the executor down. Update
DashboardServer to keep the executor as a field, initialize it in the
create/startup flow, and explicitly shut it down in close() alongside
server.stop(0). Also replace the unbounded cached pool with a bounded
ExecutorService to avoid thread exhaustion under load.
🤖 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.

Outside diff comments:
In `@sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java`:
- Around line 91-101: The DashboardServer startup path creates an
ExecutorService with Executors.newCachedThreadPool() and hands it to HttpServer,
but close() only stops the server and never shuts the executor down. Update
DashboardServer to keep the executor as a field, initialize it in the
create/startup flow, and explicitly shut it down in close() alongside
server.stop(0). Also replace the unbounded cached pool with a bounded
ExecutorService to avoid thread exhaustion under load.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33163d2a-9707-45e1-89ea-6a7a3ee3a88d

📥 Commits

Reviewing files that changed from the base of the PR and between 9f96b6b and e18f57a.

📒 Files selected for processing (7)
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Cookies.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/PasswordHasher.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Policy.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/TokenAuth.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/AuthStoreTest.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Policy.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/Cookies.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/TokenAuth.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/PasswordHasher.java
  • sdks/java/src/test/java/org/byteveda/taskito/dashboard/auth/AuthStoreTest.java
  • sdks/java/src/main/java/org/byteveda/taskito/dashboard/auth/AuthStore.java

@pratyush618 pratyush618 merged commit 78d4052 into master Jul 7, 2026
20 checks passed
@pratyush618 pratyush618 deleted the feat/java-dashboard-auth branch July 7, 2026 18:44
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