Skip to content

fix(api,ui): remaining Tier-0 security hardening (#157-#164)#238

Merged
nazarli-shabnam merged 10 commits into
mainfrom
security/remaining-hardening-157-164
Jul 3, 2026
Merged

fix(api,ui): remaining Tier-0 security hardening (#157-#164)#238
nazarli-shabnam merged 10 commits into
mainfrom
security/remaining-hardening-157-164

Conversation

@nazarli-shabnam

@nazarli-shabnam nazarli-shabnam commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Combined fix for the 8 remaining Tier-0 security issues from the ongoing security-hardening pass, per request to batch these into a single PR rather than one-at-a-time.

Fixes #157
Fixes #158
Fixes #159
Fixes #160
Fixes #161
Fixes #162
Fixes #163
Fixes #164

What changed, per issue

#157 — Workspace-level labels (project_id NULL) are not workspace-scoped (IDOR)
LabelService.ensureProjectAccess now returns the resolved workspace ID, and GetByID rejects any label whose WorkspaceID doesn't match the caller's workspace — closing the gap where a workspace-level label (project_id = NULL) from a different workspace could be read/updated/deleted just by supplying its UUID.

#158 — Invite join not bound to invited email; non-atomic accept
JoinByToken/JoinByInviteID (workspace + project) now require the authenticated user's email to match the invite's email (case-insensitive) before joining — a leaked/forwarded invite token can no longer be redeemed by an unrelated account. "Mark invite accepted" and "add member" are now wrapped in a single DB transaction instead of two independent, error-ignored writes. Also fixed (found in our own review): rejoining after a prior removal no longer 500s forever — a soft-deleted membership row is now revived instead of colliding with the unique constraint.

#159 — No rate limiting on auth endpoints
Added a small Redis-backed fixed-window rate limiter, applied per-IP to sign-in, email-check, magic-code-request, and forgot-password. Sign-in also gets a per-account lockout (10 failed attempts / 15 min, tracked independently of successful logins). Fixed the magic-code bug where re-requesting a code reset the verify-attempt counter — failed-verify attempts are now tracked on their own key/TTL that a new code request doesn't touch. Everything fails open if Redis is down or absent, consistent with how this repo treats optional infrastructure elsewhere.

#160 — File upload handler has no size limit
Generic uploads (avatars/covers/logos) are now capped at 5 MiB, rejected before the file is opened/streamed to MinIO.

#161 — ServeFile missing nosniff/Content-Disposition
Added X-Content-Type-Options: nosniff and an explicit Content-Disposition: inline to ServeFile's response. Note: ServeFile only ever serves the uploads/ prefix today (attachments/ is already rejected), so there's no exploitable stored-XSS via this handler right now — this closes the header gap defensively; fully closing the latent risk for attachments/ is issue #135's job (separate, out of scope here).

#162 — API tokens issued but never accepted by auth middleware
RequireAuth now accepts Authorization: Bearer <token> as a real API token (hashed + looked up, is_active/expired_at enforced, last_used updated) instead of only ever treating Bearer values as raw session keys. The raw-session-key fallback is kept for Bearer values that aren't a valid API token, since the OAuth cross-origin dev flow legitimately sends the session key that way. Deactivated users' tokens are rejected the same way deactivated users' sessions are (reuses the #155 fix).

#163 — InstanceAdminProtectedRoute doesn't check admin status
/api/users/me/ now reports is_instance_admin, and the frontend route guard redirects non-admins to / instead of just checking isAuthenticated. Also fixed (found in our own review): sign-in/magic-code-verify responses don't carry this field, so the frontend now re-fetches /me right after establishing a session instead of trusting the raw auth response — otherwise a real instance admin logging in and landing back on an instance-admin route would get bounced until a manual reload.

#164 — Dead mock instance-admin login page
Deleted the mock login page (accepted any non-empty credentials) and its two dangling references; the route was already commented out and unreachable, this just removes the ship-in-the-bundle risk.

Testing

  • New/extended Go tests for every backend fix (label cross-workspace rejection, invite email-mismatch + rejoin-after-removal, API-token auth incl. revoked/expired/deactivated-user cases, is_instance_admin on Me) — all passing against the real Postgres testcontainer via testutil.NewTestServer.
  • Live end-to-end verification against a real running instance (docker compose up, go run ./cmd/api) for everything that needed real Redis/MinIO (no test infra exists for either in this repo): reproduced and confirmed-fixed the rate-limit 429s (both per-IP and per-account), the magic-code lockout surviving a simulated new-code request, the upload size rejection, and the nosniff/Content-Disposition headers on a served file — all inspected directly via curl/redis-cli.
  • Ran 3 parallel /code-review (high effort) passes across the full diff; fixed all 6 real findings before pushing (Redis key-prefix collision, a permanent-lockout edge case in the rate limiter, sign-in lockout counting successes, the is_instance_admin frontend gap, and the rejoin-after-removal issue above).
  • Full go vet/go test ./... and npm run typecheck/lint/format:check all pass.

AI assistance

This change was implemented with Claude Code (Sonnet 5), which read each issue, implemented the fixes, wrote regression tests, performed live end-to-end verification against a real instance, and ran multiple independent code-review passes to catch and fix issues before this PR was opened for human review.

@nazarli-shabnam nazarli-shabnam self-assigned this Jul 2, 2026
Copilot AI review requested due to automatic review settings July 2, 2026 20:47
@strix-security

strix-security Bot commented Jul 2, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 530c6de.


Reviewed by Strix
Re-run review · Configure security review settings

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@nazarli-shabnam nazarli-shabnam added bug Something isn't working API Vulnerability labels Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Label reads now reject foreign-workspace labels, invite joins require matching emails and transactions, bearer tokens can authenticate requests, and auth/upload paths add Redis limits and file-response guards.

Changes

Label workspace access control

Layer / File(s) Summary
Resolve workspace for labels
apps/api/internal/service/label.go
ensureProjectAccess returns the resolved workspace ID, and ListByProject and Create reuse it when working with labels.
Reject foreign-workspace label reads
apps/api/internal/service/label.go, apps/api/internal/handler/label_test.go
GetByID compares the fetched label workspace against the resolved workspace and returns ErrLabelNotFound on mismatch; the handler test seeds a foreign-workspace label and asserts PATCH/DELETE return 404.

Invite email match and transactional joins

Layer / File(s) Summary
Add transaction helpers
apps/api/internal/store/workspace.go, apps/api/internal/store/project.go
WorkspaceStore and ProjectStore each gain a Transaction method that runs a callback with the store DB inside a GORM transaction.
Enforce invite email match
apps/api/internal/service/workspace.go, apps/api/internal/service/project.go
requireInviteEmailMatch helpers load the user, require an email, and compare it case-insensitively to the invite email before joins proceed.
Make join flows transactional
apps/api/internal/service/workspace.go, apps/api/internal/service/project.go, apps/api/internal/handler/join_test.go
Join-by-token and join-by-invite-ID now accept invites and create members inside transactions, and tests cover email mismatch rejection plus email-match success for workspace and project joins.

API token authentication and instance-admin state

Layer / File(s) Summary
Expose API token lookup and active users
apps/api/internal/store/api_token.go, apps/api/internal/auth/service.go
API token hashing, active-by-hash lookup, last-used updates, and active-user resolution are added.
Resolve bearer tokens in auth middleware
apps/api/internal/middleware/auth.go
Bearer auth now checks API tokens first, falls back to raw session keys, and sets the authenticated user on success.
Wire token auth and instance-admin state
apps/api/internal/handler/auth.go, apps/api/internal/router/router.go, apps/api/internal/handler/auth_test.go, apps/web/src/api/types.ts, apps/web/src/contexts/AuthContext.tsx, apps/web/src/types/index.ts, apps/web/src/routes/InstanceAdminProtectedRoute.tsx, apps/web/src/pages/instance-admin/index.ts, apps/web/src/routes/index.tsx
Auth responses and web user types carry is_instance_admin / isInstanceAdmin, protected routes pass the API token store into auth middleware, instance-admin routing checks the flag, and tests cover bearer-token success, revocation rejection, and deactivated-user rejection.

Auth throttling and upload safeguards

Layer / File(s) Summary
Add Redis rate-limit primitives
apps/api/internal/redis/cache.go
Redis key prefixes, fixed-window Allow/Count, and magic-code verify-failure counters are added.
Rate-limit auth routes
apps/api/internal/middleware/ratelimit.go, apps/api/internal/router/router.go
A Gin middleware enforces per-IP Redis limits, and selected /auth POST routes are wrapped with it.
Harden sign-in and magic-code verify
apps/api/internal/handler/auth.go
SignIn uses request-scoped context for Redis limiting, and MagicCodeVerify gates on verify-fail counts, bumps failures on mismatch, and resets on success.
Cap uploads and set file-response headers
apps/api/internal/handler/upload.go
Generic uploads are rejected above 5 MiB, and file responses include nosniff and inline headers.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related issues

Poem

A bunny hopped through routes so wide,
With tokens, joins, and guards inside.
It sniffed no content, capped the load,
And kept the counters on the road.
🐰 Hop, hop — the gates now know their code

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning No PR description was provided, so none of the required template sections are filled in. Add the template sections: Summary, Linked issues, Type of change, Surface, What changed, Why this approach, Test plan, and checklist items.
✅ Passed checks (4 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.
Title check ✅ Passed The title is broadly aligned with the PR’s API and UI security hardening changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/remaining-hardening-157-164

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

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

This PR hardens label access control by ensuring workspace-level labels are scoped to the caller's resolved workspace before update or deletion. I reviewed the changed service logic and surrounding handler/store call paths and did not find any new security issues in the modified code.


Reviewed by Strix
Configure security review settings

@nazarli-shabnam nazarli-shabnam added this to the Finish w Enhancements milestone Jul 2, 2026

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

I reviewed the changed service, store, and handler test files around label scoping and invite redemption. The diff hardens access control by binding invite redemption to the invited email and tightening workspace scoping for labels, and I did not find any new security vulnerabilities in the changed code.


Reviewed by Strix
Configure security review settings

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

🧹 Nitpick comments (2)
apps/api/internal/service/workspace.go (1)

324-329: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Service performs raw GORM writes inside the transaction callback, bypassing the store layer.

tx.Save(inv) / tx.Create(m) put persistence logic directly in the service. The layering rule keeps GORM access in stores. Consider having stores expose tx-aware methods (e.g. winv.SaveTx(tx, inv), ws.AddMemberTx(tx, m)) and calling those from within the transaction callback so the service stays free of direct GORM calls. Same pattern applies to JoinByInviteID below.

As per coding guidelines: "handlers call services, services call stores; handlers must never touch GORM directly, and stores must never call services."

🤖 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 `@apps/api/internal/service/workspace.go` around lines 324 - 329, The
transaction callback in the service is doing direct GORM persistence, which
breaks the service/store layering. Move the invite update and member insert out
of the callback body by adding tx-aware store methods such as winv.SaveTx and
ws.AddMemberTx, then call those from the transaction in workspace.go instead of
tx.Save(inv) and tx.Create(m). Apply the same pattern in JoinByInviteID so the
Workspace service only orchestrates the transaction while all GORM access stays
in the stores.

Source: Coding guidelines

apps/api/internal/handler/join_test.go (1)

42-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Accept-path tests assert only HTTP 200, not that membership was actually persisted.

Consider also asserting the WorkspaceMember / ProjectMember row exists after the join, so a regression that returns 200 without creating membership (or that creates duplicates) is caught.

Also applies to: 89-90

🤖 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 `@apps/api/internal/handler/join_test.go` around lines 42 - 43, The join-path
test only checks the HTTP 200 response and does not verify that membership was
actually created. Update the `join` test in
`apps/api/internal/handler/join_test.go` to assert the persisted
`WorkspaceMember` and/or `ProjectMember` record exists after calling
`ts.POST("/api/workspaces/join/", ...)`, using the same fixture/token flow
already in the test. Make the assertion in the accept-path cases so `join` and
related duplicate-regression scenarios are caught even when the handler returns
OK.
🤖 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.

Nitpick comments:
In `@apps/api/internal/handler/join_test.go`:
- Around line 42-43: The join-path test only checks the HTTP 200 response and
does not verify that membership was actually created. Update the `join` test in
`apps/api/internal/handler/join_test.go` to assert the persisted
`WorkspaceMember` and/or `ProjectMember` record exists after calling
`ts.POST("/api/workspaces/join/", ...)`, using the same fixture/token flow
already in the test. Make the assertion in the accept-path cases so `join` and
related duplicate-regression scenarios are caught even when the handler returns
OK.

In `@apps/api/internal/service/workspace.go`:
- Around line 324-329: The transaction callback in the service is doing direct
GORM persistence, which breaks the service/store layering. Move the invite
update and member insert out of the callback body by adding tx-aware store
methods such as winv.SaveTx and ws.AddMemberTx, then call those from the
transaction in workspace.go instead of tx.Save(inv) and tx.Create(m). Apply the
same pattern in JoinByInviteID so the Workspace service only orchestrates the
transaction while all GORM access stays in the stores.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a3e8c15e-9161-4788-9429-33ee53568410

📥 Commits

Reviewing files that changed from the base of the PR and between c8c1ec2 and 9274b2e.

📒 Files selected for processing (5)
  • apps/api/internal/handler/join_test.go
  • apps/api/internal/service/project.go
  • apps/api/internal/service/workspace.go
  • apps/api/internal/store/project.go
  • apps/api/internal/store/workspace.go

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

Reviewed the changed auth, upload, middleware, Redis, router, and invite/label access-control code paths added since the prior pass. The new changes harden rate limiting, invite redemption, workspace scoping, and file handling, and I did not find any security vulnerabilities in the changed code.


Reviewed by Strix
Configure security review settings

@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

Caution

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

⚠️ Outside diff range comments (1)
apps/api/internal/handler/upload.go (1)

48-56: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Size check runs too late to prevent resource exhaustion.

file.Size is only known after c.FormFile("file") (Line 48) has already fully parsed the multipart body — Gin buffers/spills the entire upload to memory or a temp file before this check executes at Line 53. An attacker can still force the server to read and buffer an arbitrarily large payload per request even though it's ultimately rejected.

Enforce the limit before parsing, e.g. wrap the request body with http.MaxBytesReader (or check c.Request.ContentLength) prior to calling c.FormFile.

🛡️ Proposed fix to reject oversized bodies before parsing
+	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxUploadSize)
 	file, err := c.FormFile("file")
 	if err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided", "detail": err.Error()})
+		c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided or file too large", "detail": err.Error()})
 		return
 	}
-	if file.Size > maxUploadSize {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "File too large. Maximum size is 5MB."})
-		return
-	}
🤖 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 `@apps/api/internal/handler/upload.go` around lines 48 - 56, The upload size
validation happens after c.FormFile("file") has already parsed the multipart
body, so oversized requests can still consume memory/disk before rejection.
Update the upload handler in the code path around c.FormFile and the
maxUploadSize check to enforce the limit before parsing, using
http.MaxBytesReader on the request body or an early ContentLength guard before
calling c.FormFile. Keep the existing file.Size check as a secondary validation,
but make the pre-parse limit the primary protection.
🧹 Nitpick comments (1)
apps/api/internal/redis/cache.go (1)

247-258: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Allow is non-atomic: a failed/interrupted Expire leaves the key without a TTL, causing a permanent lockout.

Incr and Expire are two separate round-trips. If the process crashes or Expire errors after the first Incr, the counter key survives with no TTL. Since Expire is only attempted when n == 1, subsequent increments never re-arm the TTL, so the counter climbs past limit forever — permanently throttling that IP/account. Consider making this atomic (Lua script or TxPipeline) and/or defensively re-setting the TTL when it is missing.

♻️ Atomic fixed-window via Lua
var allowScript = redis.NewScript(`
local n = redis.call("INCR", KEYS[1])
if n == 1 then
  redis.call("PEXPIRE", KEYS[1], ARGV[1])
end
return n
`)

func (c *Client) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
	n, err := allowScript.Run(ctx, c.Client, []string{key}, window.Milliseconds()).Int64()
	if err != nil {
		return false, err
	}
	return n <= int64(limit), nil
}

Please confirm go-redis v9.17.3 NewScript/Run signatures before applying.

🤖 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 `@apps/api/internal/redis/cache.go` around lines 247 - 258, The Allow method in
Client is doing INCR and EXPIRE as separate non-atomic steps, which can leave a
counter key without a TTL if Expire fails or the process is interrupted. Update
Allow to make the increment-and-expiry logic atomic, ideally by using a
redis.NewScript with Run (or a TxPipeline) around the existing c.Client call
path, and ensure the TTL is always set when the counter is first created. Keep
the method behavior and symbols the same (Client.Allow, c.Client, limit/window
handling), but remove the race where a missing expiry can permanently throttle a
key.
🤖 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 `@apps/api/internal/middleware/ratelimit.go`:
- Around line 21-22: The rate-limit key generation in ratelimit middleware
currently relies on c.ClientIP(), but router.New() leaves Gin’s trusted proxies
unset so forwarded IP headers can be spoofed. Update the router initialization
to explicitly set trusted proxies (ingress/LB CIDRs) or disable forwarded
headers with SetTrustedProxies(nil), and keep the key construction in the
ratelimit handler using the now-trusted client IP.

---

Outside diff comments:
In `@apps/api/internal/handler/upload.go`:
- Around line 48-56: The upload size validation happens after c.FormFile("file")
has already parsed the multipart body, so oversized requests can still consume
memory/disk before rejection. Update the upload handler in the code path around
c.FormFile and the maxUploadSize check to enforce the limit before parsing,
using http.MaxBytesReader on the request body or an early ContentLength guard
before calling c.FormFile. Keep the existing file.Size check as a secondary
validation, but make the pre-parse limit the primary protection.

---

Nitpick comments:
In `@apps/api/internal/redis/cache.go`:
- Around line 247-258: The Allow method in Client is doing INCR and EXPIRE as
separate non-atomic steps, which can leave a counter key without a TTL if Expire
fails or the process is interrupted. Update Allow to make the
increment-and-expiry logic atomic, ideally by using a redis.NewScript with Run
(or a TxPipeline) around the existing c.Client call path, and ensure the TTL is
always set when the counter is first created. Keep the method behavior and
symbols the same (Client.Allow, c.Client, limit/window handling), but remove the
race where a missing expiry can permanently throttle a key.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 111fc6cc-2a06-4bfd-b3b4-6eaa1b94e5be

📥 Commits

Reviewing files that changed from the base of the PR and between 9274b2e and 95f65c8.

📒 Files selected for processing (5)
  • apps/api/internal/handler/auth.go
  • apps/api/internal/handler/upload.go
  • apps/api/internal/middleware/ratelimit.go
  • apps/api/internal/redis/cache.go
  • apps/api/internal/router/router.go

Comment thread apps/api/internal/middleware/ratelimit.go

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

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment thread apps/api/internal/middleware/ratelimit.go

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

Caution

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

⚠️ Outside diff range comments (1)
apps/api/internal/router/router.go (1)

498-506: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add throttling to /auth/sign-up/
/auth/sign-up/ is still public and can be spammed for mass account creation. Add the same middleware.RateLimit wrapper used by the other auth POST routes.

🤖 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 `@apps/api/internal/router/router.go` around lines 498 - 506, Add rate limiting
to the public sign-up endpoint: the authGroup.POST call for authHandler.SignUp
should use middleware.RateLimit like the other auth POST routes. Reuse cfg.Redis
with a unique bucket name for sign-up and the same 15*time.Minute window so
/auth/sign-up/ is throttled consistently with EmailCheck, SignIn,
ForgotPassword, and MagicCodeRequest.
🧹 Nitpick comments (1)
apps/api/internal/handler/auth_test.go (1)

306-373: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider adding a test for expired-token rejection.

GetActiveByHash filters on expired_at, but none of the new tests exercise an expired token being rejected — only revoked and deactivated-user cases are covered.

🤖 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 `@apps/api/internal/handler/auth_test.go` around lines 306 - 373, Add a test
case for expired API tokens in auth_test by extending the existing token auth
coverage around TestAuth_ApiToken_AuthenticatesRequests and
TestAuth_ApiToken_RevokedRejected. Create a token with an expired expired_at
value (or otherwise mark it expired in the test DB), then verify that a request
using the Bearer token to /api/users/me/ is rejected with unauthorized. Use the
existing token creation and request helpers so the test exercises
GetActiveByHash’s expired_at filtering.
🤖 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 `@apps/api/internal/middleware/auth.go`:
- Around line 48-57: The API-token authentication block in the auth middleware
is bypassing the service layer by calling the store directly. Move the hash
lookup, active-user fetch, and last-used update into auth.Service, using
existing service patterns like UserFromSession and ActiveUserByID, and add a
service method such as UserFromAPIToken(ctx, bearer string). Update the
middleware in auth.go to call only the service, and have the service use the
ApiTokenStore internally (similar to sessionStore and userStore).
- Around line 40-63: The auth middleware currently uses an if/else if chain in
the request handler, so a stale session cookie prevents the bearer-token path
from being checked. Update the logic in the auth middleware’s cookie/bearer
block to try cookie auth first, but if `authSvc.UserFromSession` fails or
returns nil, continue on to the `Authorization` header handling instead of
falling through to 401. Keep the existing `apiTokens.GetActiveByHash`,
`authSvc.ActiveUserByID`, and fallback `UserFromSession` bearer-session lookup
behavior intact.

---

Outside diff comments:
In `@apps/api/internal/router/router.go`:
- Around line 498-506: Add rate limiting to the public sign-up endpoint: the
authGroup.POST call for authHandler.SignUp should use middleware.RateLimit like
the other auth POST routes. Reuse cfg.Redis with a unique bucket name for
sign-up and the same 15*time.Minute window so /auth/sign-up/ is throttled
consistently with EmailCheck, SignIn, ForgotPassword, and MagicCodeRequest.

---

Nitpick comments:
In `@apps/api/internal/handler/auth_test.go`:
- Around line 306-373: Add a test case for expired API tokens in auth_test by
extending the existing token auth coverage around
TestAuth_ApiToken_AuthenticatesRequests and TestAuth_ApiToken_RevokedRejected.
Create a token with an expired expired_at value (or otherwise mark it expired in
the test DB), then verify that a request using the Bearer token to
/api/users/me/ is rejected with unauthorized. Use the existing token creation
and request helpers so the test exercises GetActiveByHash’s expired_at
filtering.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba49d3ab-1a52-4acb-88b4-a7c32f1fe573

📥 Commits

Reviewing files that changed from the base of the PR and between 95f65c8 and 706d42e.

📒 Files selected for processing (5)
  • apps/api/internal/auth/service.go
  • apps/api/internal/handler/auth_test.go
  • apps/api/internal/middleware/auth.go
  • apps/api/internal/router/router.go
  • apps/api/internal/store/api_token.go

Comment thread apps/api/internal/middleware/auth.go
Comment thread apps/api/internal/middleware/auth.go Outdated

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

🧹 Nitpick comments (2)
apps/api/internal/handler/auth.go (2)

124-133: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Rate-limit fails open and swallows the Redis error.

When Allow returns an error, err == nil && !ok skips the block and the error is dropped silently. That's a reasonable availability tradeoff, but logging the error preserves observability for a security control and helps detect a degraded Redis.

♻️ Suggested logging
 		ok, err := h.Redis.Allow(ctx, redis.PrefixRateLimit+"signinacct:"+emailNorm, 10, 15*time.Minute)
-		if err == nil && !ok {
+		if err != nil {
+			h.log().Error("signin account rate-limit check", "error", err)
+		} else if !ok {
 			c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too many sign-in attempts for this account, please try again later"})
 			return
 		}
🤖 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 `@apps/api/internal/handler/auth.go` around lines 124 - 133, The sign-in rate
limit in auth.go currently fails open and drops Redis errors silently in the
h.Redis.Allow path. Update the SignIn flow to log any non-nil error from Allow
with clear context (for example, the signinacct key or normalized email) before
continuing, while preserving the existing availability behavior in h.Auth.SignIn
and the rate-limit decision logic.

236-241: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Me silently drops the IsAdmin error.

isAdmin, _ := h.InstanceAdmins.IsAdmin(...) defaults to false on a transient DB error, so admins can intermittently lose is_instance_admin in the UI. It fails safe security-wise, but consider logging the error for observability.

🤖 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 `@apps/api/internal/handler/auth.go` around lines 236 - 241, The Me response
path in auth.go is ignoring errors from h.InstanceAdmins.IsAdmin, which can
silently flip is_instance_admin to false on transient failures. Update the Me
handler to capture the error instead of discarding it, and log it with enough
context (for example using the handler’s logger or request context) while still
returning the response. Keep the existing IsAdmin call and
resp["is_instance_admin"] assignment, but make the error observable rather than
ignored.
🤖 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.

Nitpick comments:
In `@apps/api/internal/handler/auth.go`:
- Around line 124-133: The sign-in rate limit in auth.go currently fails open
and drops Redis errors silently in the h.Redis.Allow path. Update the SignIn
flow to log any non-nil error from Allow with clear context (for example, the
signinacct key or normalized email) before continuing, while preserving the
existing availability behavior in h.Auth.SignIn and the rate-limit decision
logic.
- Around line 236-241: The Me response path in auth.go is ignoring errors from
h.InstanceAdmins.IsAdmin, which can silently flip is_instance_admin to false on
transient failures. Update the Me handler to capture the error instead of
discarding it, and log it with enough context (for example using the handler’s
logger or request context) while still returning the response. Keep the existing
IsAdmin call and resp["is_instance_admin"] assignment, but make the error
observable rather than ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 31cd3cc9-0004-4be1-87df-bcbfbc622777

📥 Commits

Reviewing files that changed from the base of the PR and between 706d42e and 274a6d8.

📒 Files selected for processing (10)
  • apps/api/internal/handler/auth.go
  • apps/api/internal/handler/auth_test.go
  • apps/api/internal/router/router.go
  • apps/web/src/api/types.ts
  • apps/web/src/contexts/AuthContext.tsx
  • apps/web/src/pages/instance-admin/InstanceAdminLoginPage.tsx
  • apps/web/src/pages/instance-admin/index.ts
  • apps/web/src/routes/InstanceAdminProtectedRoute.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/types/index.ts
💤 Files with no reviewable changes (3)
  • apps/web/src/pages/instance-admin/InstanceAdminLoginPage.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/pages/instance-admin/index.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/contexts/AuthContext.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/internal/router/router.go
  • apps/api/internal/handler/auth_test.go

@nazarli-shabnam nazarli-shabnam changed the title fix(api): scope workspace-level labels to the caller's workspace (#157) fix(api,ui): remaining Tier-0 security hardening (#157-#164) Jul 3, 2026
@nazarli-shabnam

Copy link
Copy Markdown
Member Author

Addressed all 4 CodeRabbit/Strix findings in 99ff69b:

  • Spoofable rate-limit key: added r.SetTrustedProxies(nil) in router.go so c.ClientIP() can no longer be fooled by client-supplied X-Forwarded-For/X-Real-IP headers.
  • Stale cookie masking a valid bearer token: cookie and bearer checks are now two independent if blocks instead of if/else if, so an expired/invalid session cookie no longer blocks a valid API-token Authorization header. Added a regression test (TestAuth_ApiToken_ValidEvenWithStaleCookiePresent).
  • Middleware bypassing the service layer: moved the API-token hash-lookup/active-user/last-used sequence out of middleware/auth.go into a new auth.Service.UserFromAPIToken, so middleware only ever talks to authSvc — matching this repo's handler/middleware → service → store layering rule.

@nazarli-shabnam nazarli-shabnam merged commit cacf519 into main Jul 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment