Skip to content

feat: operationalize SecurityAuditService across core auth flows#724

Merged
2witstudios merged 5 commits intomasterfrom
ppg/operationalize-audit-service
Feb 26, 2026
Merged

feat: operationalize SecurityAuditService across core auth flows#724
2witstudios merged 5 commits intomasterfrom
ppg/operationalize-audit-service

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 25, 2026

Closes #535

Summary

  • Operationalized the tamper-evident SecurityAuditService across core auth request flows
  • Created an adapter bridging existing logAuthEvent/logSecurityEvent to securityAudit.logEvent()
  • Wired direct securityAudit calls into login, logout, and admin auth flows

Changes

New files

  • packages/lib/src/audit/security-audit-adapter.ts — Adapter with auditAuthEvent() and auditSecurityEvent() that fan out to securityAudit.logEvent(). Maps auth/security event types to SecurityEventType enums. Uses fire-and-forget pattern (.catch(() => {})) to avoid blocking auth flows.
  • packages/lib/src/audit/tests/security-audit-adapter.test.ts — 11 tests covering: login success/failure/logout/refresh dispatch, email masking, error resilience, security event mapping, and unmapped event filtering.

Modified files

  • packages/lib/src/audit/index.ts — Added exports for auditAuthEvent and auditSecurityEvent
  • packages/lib/src/server.ts — Added securityAudit, auditAuthEvent, auditSecurityEvent to server barrel exports
  • apps/web/src/app/api/auth/login/route.ts — Added securityAudit.logAuthFailure() on failed login, securityAudit.logAuthSuccess() and securityAudit.logTokenCreated() on successful login
  • apps/web/src/app/api/auth/logout/route.ts — Added securityAudit.logLogout() and securityAudit.logTokenRevoked() on logout
  • apps/web/src/lib/auth/auth.ts — Added securityAudit.logAccessDenied() on CSRF validation failure and admin role version mismatch in verifyAdminAuth

Test mock updates (backwards compatibility)

  • apps/web/src/app/api/auth/tests/login.test.ts — Added securityAudit mock
  • apps/web/src/app/api/auth/tests/logout.test.ts — Added securityAudit mock
  • apps/web/src/lib/auth/tests/auth.test.ts — Added securityAudit mock

Design decisions

  • All audit calls use fire-and-forget (.catch(() => {})) so audit failures never block auth flows
  • The adapter maps a subset of security events (rate_limit, unauthorized, account_locked, suspicious_activity, invalid_token) to SecurityAuditService event types. Unmapped events (CSRF-specific) are silently skipped — already logged by logSecurityEvent and don't need hash-chain audit entries.

Test plan

  • 11/11 new adapter tests pass
  • 22/22 existing audit tests pass
  • 8/8 logout tests pass
  • No regressions in existing test mocks
  • 3/3 login-redirect tests pass (fixed missing securityAudit mock)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added security audit logging across authentication flows (login success/failure, logout, token events) with session and client details.
    • Added access-denied auditing for admin-related authentication checks to improve security visibility.
  • Tests
    • New test coverage for the security audit adapter, mapping events and ensuring resilient, non-blocking behavior.
  • Chores
    • Enabled manual triggering of the test workflow.

Wire the tamper-evident SecurityAuditService into login, logout, and
admin auth flows. Create an adapter bridging existing logAuthEvent/
logSecurityEvent to securityAudit.logEvent() with fire-and-forget
semantics so audit failures never block auth flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 11 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 0f55fb1 and 23b3237.

📒 Files selected for processing (5)
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/lib/auth/auth.ts
  • packages/lib/src/audit/index.ts
📝 Walkthrough

Walkthrough

Adds a security-audit adapter and integrates non-blocking securityAudit calls into login, logout, and admin auth failure paths; expands server mock surface and re-exports audit functions from the library.

Changes

Cohort / File(s) Summary
Auth route handlers
apps/web/src/app/api/auth/login/route.ts, apps/web/src/app/api/auth/logout/route.ts
Imports securityAudit and emits fire-and-forget audit calls: login logs auth success/failure and token creation; logout logs logout and token revocation.
Auth business logic
apps/web/src/lib/auth/auth.ts
Calls securityAudit.logAccessDenied(...) on CSRF and admin-role validation failures in verifyAdminAuth.
Server mocks in tests
apps/web/src/app/api/auth/__tests__/login-redirect.test.ts, apps/web/src/app/api/auth/__tests__/login.test.ts, apps/web/src/app/api/auth/__tests__/logout.test.ts, apps/web/src/lib/auth/__tests__/auth.test.ts
Extends @pagespace/lib/server mock with securityAudit methods (logAuthSuccess, logAuthFailure, logTokenCreated, logAccessDenied, logLogout, logTokenRevoked) returning resolved undefined.
Security audit adapter & tests
packages/lib/src/audit/security-audit-adapter.ts, packages/lib/src/audit/__tests__/security-audit-adapter.test.ts
New adapter exposing auditAuthEvent and auditSecurityEvent that map runtime events to security event types, mask email, extract correlation fields, set risk scores, and call securityAudit.logEvent with errors swallowed. Comprehensive unit tests added.
Library exports
packages/lib/src/audit/index.ts, packages/lib/src/server.ts
Re-exports auditAuthEvent, auditSecurityEvent, and securityAudit from the audit module.
CI workflow
.github/workflows/test.yml
Adds workflow_dispatch trigger to the test workflow.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Route as Auth Route
  participant Logic as Auth Logic
  participant Adapter as Security Audit Adapter
  participant AuditSvc as SecurityAuditService
  participant DB as Audit DB

  Client->>Route: POST /auth/login (credentials, headers)
  Route->>Logic: validate credentials, create session
  alt login success
    Logic->>Adapter: auditAuthEvent(login_success, userId, email, ip)
    Adapter->>AuditSvc: securityAudit.logEvent({eventType: "auth.login.success", userId, details})
    AuditSvc->>DB: persist security_audit_log entry
    AuditSvc-->>Adapter: ack
    Adapter-->>Logic: (fire-and-forget)
  else login failure
    Logic->>Adapter: auditAuthEvent(login_failure, maybeEmail, ip, reason)
    Adapter->>AuditSvc: securityAudit.logEvent({eventType: "auth.login.failure", details})
    AuditSvc->>DB: persist security_audit_log entry
    AuditSvc-->>Adapter: ack
    Adapter-->>Logic: (fire-and-forget)
  end
  Route-->>Client: response (redirect / token)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐇 I hopped through logs at break of day,

I stamped each login in a tamper-proof way,
A quiet whisper — fire-and-forget,
No blocking hops, no tangled net,
I left a trail where rabbits vet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: operationalize SecurityAuditService across core auth flows' clearly and specifically describes the main objective of wiring the SecurityAuditService into core auth flows.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #535: defines event mapping, provides adapter functions for auth/security events, wires critical flows (login, logout, access denied), implements non-blocking audit calls with error handling, and includes comprehensive unit tests.
Out of Scope Changes check ✅ Passed All changes are scoped to operationalizing SecurityAuditService per issue #535; the only tangential change is adding workflow_dispatch to test.yml, which enables manual CI triggering and does not detract from the core objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/operationalize-audit-service

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 and usage tips.

@2witstudios
Copy link
Owner Author

CI Fix: Missing securityAudit mock in login-redirect.test.ts

The Unit Tests check was failing because login-redirect.test.ts was missing the securityAudit mock in its @pagespace/lib/server mock factory. The login route now calls securityAudit.logAuthSuccess(), .logAuthFailure(), and .logTokenCreated() — without the mock these were undefined, causing a 500 crash.

Fix (commit ecf3ac34): Added the securityAudit mock object to match what login.test.ts already has:

securityAudit: {
  logAuthSuccess: vi.fn().mockResolvedValue(undefined),
  logAuthFailure: vi.fn().mockResolvedValue(undefined),
  logTokenCreated: vi.fn().mockResolvedValue(undefined),
  logAccessDenied: vi.fn().mockResolvedValue(undefined),
},

File: apps/web/src/app/api/auth/__tests__/login-redirect.test.ts (lines 60-65)

All local tests continue to pass (11/11 adapter, 8/8 logout). Waiting for CI to re-run.

@2witstudios 2witstudios reopened this Feb 25, 2026
The login route now imports securityAudit from @pagespace/lib/server,
but login-redirect.test.ts was missing the mock, causing all 3 tests
to crash with 500 instead of 200.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@2witstudios 2witstudios force-pushed the ppg/operationalize-audit-service branch from f0e70a8 to aadb476 Compare February 25, 2026 23:19
2witstudios and others added 2 commits February 25, 2026 18:04
Merge two separate export lines from './audit' into one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allows manual triggering of the test suite workflow for cases where
the pull_request event doesn't fire automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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)
packages/lib/src/audit/security-audit-adapter.ts (1)

76-76: Email masking pattern preserves 2 characters; elsewhere 3 characters are preserved.

The masking pattern /(.{2}).*(@.*)/, '$1***$2' preserves the first 2 characters of the email local part. However, based on learnings from signup-passkey/route.ts, the established pattern masks email as "first 3 chars + '***@' + domain".

Consider aligning with the existing pattern for consistency:

♻️ Proposed fix for consistent masking
-  const maskedEmail = email ? email.replace(/(.{2}).*(@.*)/, '$1***$2') : undefined;
+  const maskedEmail = email ? email.replace(/(.{3}).*(@.*)/, '$1***$2') : undefined;

Based on learnings: "email masked as first 3 chars + '***@' + domain" in signup-passkey/route.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/src/audit/security-audit-adapter.ts` at line 76, The email
masking currently assigned to maskedEmail uses the regex /(.{2}).*(@.*)/ and
preserves 2 characters; update the pattern to preserve 3 characters to match the
established convention (first 3 chars + '***' + domain) by changing the regex
used in the maskedEmail assignment (variable maskedEmail in
security-audit-adapter.ts) so it captures three leading chars (e.g.,
/(.{3}).*(@.*)/) and keeps the rest of the masking logic unchanged for
consistency with signup-passkey/route.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/app/api/auth/login/route.ts`:
- Line 152: The login route is sending raw PII to audit/tracking calls; before
calling securityAudit.logAuthFailure, logAuthEvent, and trackAuthEvent replace
email with a masked version using the same pattern as signup-passkey (e.g., take
email.substring(0,3) + '***@' + (email.split('@')[1] || '***') into a
maskedEmail variable) and pass maskedEmail instead of email to
securityAudit.logAuthFailure, logAuthEvent and trackAuthEvent to comply with PII
retention policies.

---

Nitpick comments:
In `@packages/lib/src/audit/security-audit-adapter.ts`:
- Line 76: The email masking currently assigned to maskedEmail uses the regex
/(.{2}).*(@.*)/ and preserves 2 characters; update the pattern to preserve 3
characters to match the established convention (first 3 chars + '***' + domain)
by changing the regex used in the maskedEmail assignment (variable maskedEmail
in security-audit-adapter.ts) so it captures three leading chars (e.g.,
/(.{3}).*(@.*)/) and keeps the rest of the masking logic unchanged for
consistency with signup-passkey/route.ts.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5288e and 352f462.

📒 Files selected for processing (11)
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/logout.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/lib/auth/__tests__/auth.test.ts
  • apps/web/src/lib/auth/auth.ts
  • packages/lib/src/audit/__tests__/security-audit-adapter.test.ts
  • packages/lib/src/audit/index.ts
  • packages/lib/src/audit/security-audit-adapter.ts
  • packages/lib/src/server.ts

const reason = !user ? 'invalid_email' : 'invalid_password';
logAuthEvent('failed', user?.id, email, clientIP, reason === 'invalid_email' ? 'Invalid email' : 'Invalid password');
trackAuthEvent(user?.id, 'failed_login', { reason, email, ip: clientIP });
securityAudit.logAuthFailure(email, clientIP, reason).catch(() => {});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the security audit service implementation
find . -type f -name "*security*audit*" -o -name "*audit*" | grep -E "\.(ts|tsx)$" | head -20

Repository: 2witstudios/PageSpace

Length of output: 591


🏁 Script executed:

# Search for logAuthFailure import and implementation
rg -n "logAuthFailure" --type ts -A 2

Repository: 2witstudios/PageSpace

Length of output: 2312


🏁 Script executed:

# Check the `@pagespace/lib/server` imports to find where securityAudit comes from
rg -n "from.*@pagespace/lib/server" --type ts

Repository: 2witstudios/PageSpace

Length of output: 45704


🏁 Script executed:

# Read the logAuthFailure implementation from security-audit.ts
sed -n '223,260p' packages/lib/src/audit/security-audit.ts

Repository: 2witstudios/PageSpace

Length of output: 807


🏁 Script executed:

# Search for email masking logic in the security audit service
rg -n "mask.*email|email.*mask|slice.*replace" packages/lib/src/audit/security-audit.ts

Repository: 2witstudios/PageSpace

Length of output: 47


🏁 Script executed:

# Check the full logAuthFailure method and surrounding context
sed -n '220,280p' packages/lib/src/audit/security-audit.ts

Repository: 2witstudios/PageSpace

Length of output: 1262


🏁 Script executed:

# Find the logEvent implementation
sed -n '100,200p' packages/lib/src/audit/security-audit.ts | head -60

Repository: 2witstudios/PageSpace

Length of output: 1969


🏁 Script executed:

# Search for any email masking logic in the entire security-audit.ts file
rg -n "slice|mask|replace|PII" packages/lib/src/audit/security-audit.ts | head -20

Repository: 2witstudios/PageSpace

Length of output: 47


🏁 Script executed:

# Check how other routes handle email masking before calling securityAudit methods
rg -B3 "securityAudit.logAuth" apps/web/src/app/api/auth/ --type ts | head -40

Repository: 2witstudios/PageSpace

Length of output: 853


🏁 Script executed:

# Check logAuthSuccess implementation and how email is handled there
sed -n '200,250p' packages/lib/src/audit/security-audit.ts

Repository: 2witstudios/PageSpace

Length of output: 1119


🏁 Script executed:

# Check if there's any email masking done in the login route before the calls
sed -n '140,160p' apps/web/src/app/api/auth/login/route.ts

Repository: 2witstudios/PageSpace

Length of output: 1049


🏁 Script executed:

# Compare with other auth routes to see the masking pattern
rg -B5 "securityAudit\.|trackAuthEvent" apps/web/src/app/api/auth/signup-passkey/route.ts | head -30

Repository: 2witstudios/PageSpace

Length of output: 810


Mask email before passing to securityAudit.logAuthFailure to comply with PII retention policies.

The email is passed directly to logAuthFailure without masking. Unlike the signup-passkey route which masks PII before logging (using the pattern email.substring(0, 3) + '***@' + domain), the login route passes the full email unmasked. This is inconsistent and fails to comply with data retention policies.

Apply the same masking pattern used in signup-passkey/route.ts before the call:

const maskedEmail = email.substring(0, 3) + '***@' + (email.split('@')[1] || '***');
securityAudit.logAuthFailure(maskedEmail, clientIP, reason).catch(() => {});

Note: Also review logAuthEvent and trackAuthEvent calls on the same line block, as they also receive the unmasked email.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/auth/login/route.ts` at line 152, The login route is
sending raw PII to audit/tracking calls; before calling
securityAudit.logAuthFailure, logAuthEvent, and trackAuthEvent replace email
with a masked version using the same pattern as signup-passkey (e.g., take
email.substring(0,3) + '***@' + (email.split('@')[1] || '***') into a
maskedEmail variable) and pass maskedEmail instead of email to
securityAudit.logAuthFailure, logAuthEvent and trackAuthEvent to comply with PII
retention policies.

Master moved securityAudit/maskEmail imports to @pagespace/lib/audit.
Resolved duplicate import in login/route.ts and updated
login-redirect.test.ts mock to match new import paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@2witstudios 2witstudios merged commit fd6b29a into master Feb 26, 2026
8 of 9 checks passed
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.

[Audit] Operationalize SecurityAuditService

1 participant