Repository navigation
fix(auth): keep credentials out of authentication logs - #567
Raghav-Bansal-15 wants to merge 92 commits into
Conversation
|
@claude review: Please review credential redaction, independent correlation IDs, passkey diagnostic allowlisting, and regression coverage on this standalone extraction. |
|
Claude finished @kshivang's task in 6m 42s —— View job Review: credential redaction extraction
The direction is right and the two new test files are the right shape (canary strings asserted absent from captured output, rather than expectation snapshots). Five things I'd want resolved before this merges, two of them because the change's own premise is contradicted elsewhere in the same diff. 1.
|
| name | sanitizeMap |
sanitizeExceptionMessage / sanitizeStackTrace |
|---|---|---|
sessionId, session_id, SESSION_ID, sessionid |
redacted | redacted |
crossDeviceSessionId, authSessionId |
redacted | not redacted |
session-id, session.id |
not redacted (substring fails on -/.) |
not redacted |
crossDeviceSessionId is the actual variable name at DesktopPasskeyService.kt:154, so it is the spelling most likely to reach an exception message — and crash reports run through sanitizeExceptionMessage and egress. A pair match over the existing split covers every prefix and every separator with no special case and no new vocabulary entries:
val words = name.replace(camelCaseBoundary, "_").split('_', '-', '.')
return words.any { w -> sensitiveValueNames.any { w.equals(it, ignoreCase = true) } } ||
words.zipWithNext().any { (a, b) -> a.equals("session", true) && b.equals("id", true) }That also lets the sessionid/session_id entries come back out of sensitiveValueNames (:350-351), keeping that set the single shared vocabulary the KDoc at :333-338 promises.
Smaller points
maskToken as a constant removes the last signal from sanitizeMap's length rule. sanitizeMap:401 fires on any string ≥ 20 chars under any key, so a 20-char plugin id or file name now logs as [REDACTED] with no way to tell two different values apart. maskCredentialId:78 already shows this file's preferred compromise: [CREDENTIAL_ID:${length}chars]. A [REDACTED:34chars] form satisfies "no prefix, no suffix, no fragment" and keeps two log lines distinguishable. If a length is itself considered a leak here, say so in the KDoc, since maskCredentialId publishes one.
Dropping error = e is correct for this pipeline, but write down why, and note the class isn't closed. I checked: BossLogger.log hands entry.error straight to SLF4J (BossLogger.kt:447-463) and writeToFileAsync appends entry.error.message plus frames with no sanitizer at all (:517-531); entry.data is interpolated raw; and LogSanitizer.sanitizeMap has no production caller in the tree. So the local log file is protected only by discipline at ~550 other logger.*(… error = e) sites. Two asks: a one-line comment at the changed sites so the next person doesn't restore the throwable, and a follow-up issue to sanitize inside BossLogger (message, data via sanitizeMap, error via sanitizeExceptionMessage/sanitizeStackTrace) which would make call-site discipline unnecessary. The crash-report egress path is already safe — CrashReport.SanitizedLogEntry drops data and error outright — so this is specifically the log file and support bundle.
Separately, e.javaClass.simpleName alone throws away the cause chain, which is what actually diagnoses an openBrowserWithProcessBuilder failure. mapOf("errorType" to e.javaClass.simpleName, "error" to LogSanitizer.sanitizeExceptionMessage(e.message)) keeps the message through the sanitizer that exists for exactly this.
Import placement. import { authFailureDetails } from … was inserted above the module docblock in index.ts, utils/authorization.ts, utils/crypto.ts and utils/error-handler.ts, splitting each file's header from its file. Move it into the existing import group. utils/logging.ts also uses semicolons and double quotes where every neighbouring file uses neither, and deno lint is continue-on-error in edge-functions.yml, so nothing will flag it.
authFailureDetails(undefined) → {failed:false} now appears on success lines: database.ts:47 logs { found: true, error: {failed:false} }. Returning undefined for a nullish error keeps that line clean.
userId asymmetry. registration.ts:55, challenge.ts:30 and database.ts:511 still log a full user id, while the host's maskUserId truncates one to 4 chars. Consistent with "harmless identifiers remain intact", but the two halves now state opposite policies for the same value — one sentence in the PR body would make that a decision rather than an omission.
Regression coverage
CredentialRedactionTest is well-targeted, and I hand-traced all four spellings through camelCaseBoundary → nameMarksSecret → sensitiveUriParamNames; they all pass, including SESSION_ID (no lowercase, so no camel boundary, so the new whole-name equality is what catches it — worth a comment, because it's the non-obvious one). Gaps: nothing pins the two paths against prefixed names (crossDeviceSessionId), which is where point 5 bites and where a test added now would fail and document the gap; session-id/session.id are untested; and nothing covers the maskUriParams URL from point 1.
logging.test.ts is gated (edge-functions.yml runs deno test --allow-all with working-directory: supabase/functions/passkey) and the canary design is right. Two weaknesses: assert(output.includes("23505")) and ("409") are asserted against a console.error the test itself makes, so they exercise authFailureDetails in isolation rather than proving any production call site still emits a usable code — driving a storeChallenge failure with code: "23505" through the captured log would be the real version; and nothing pins the dropping of an unlisted code, which is the behaviour most likely to surprise an operator. assertEquals(replay.refreshToken, refreshToken) is a good touch: it proves the redaction did not reach the response.
I could not execute either suite here — ./gradlew and deno are both unavailable/denied in this sandbox — so the pass claims in the PR body are unverified by me; everything above is from reading and hand-tracing, and the flipped expectations in LogSanitizerTest all agree with the new implementation.
• branch fix/security-credential-logging
|
Addressed the concrete redaction gaps in reviewer commit
The process-wide BossLogger raw-throwable/data sink and broader response-sanitization audit remain separate follow-up work; this PR fixes the listed authentication call sites, not every host log sink. Harmless user identifiers remain unchanged by design. Validation: passkey Deno suite 150/150 passed with mocked database calls; no live migrations. Kotlin tests and root ktlint/detekt remain queued behind the shared local build lock; hosted validation for the new head is pending. No repeated Claude review requested. |
|
Validation handoff to @Raghav-Bansal-15: your change usefully removes bearer-token fragments and database error payloads from authentication diagnostics. Your original commit and credit are preserved; reviewer fixes are separately recorded in The concrete Claude redaction findings were repaired and covered by regressions (see 5651013587). Local validation passed 150 passkey Deno tests, 22 plugin-logging tests, 80 host sanitizer tests, and root detekt. Root ktlint found one reviewer-introduced wrapping error; I cannot merge without passing required CI on the current head Next steps: complete the queued ktlint/detekt recheck and final-head Build & Test checks, investigate any actual failure, then refresh dev/mergeability and verify the now-merged #541 URI-boundary fix remains present alongside these redactions. Retain the 150 Deno tests and the Kotlin passkey URL/session-name regressions. Once green, restore ready status and merge into dev under the shared lock. Manual disposable-auth log checks remain recorded in the PR/inventory; no live migration is needed. Converting this PR to draft under the requested bounded-review handoff policy until that validation is complete. |
cd88490 to
5a2e818
Compare
2688753 to
8f28613
Compare
|
Rebased this PR and its reviewer fixes onto current dev, preserving authorship, and marked it ready for review. The current-head full fork build and test run is green. The credential/URL redaction fixes are retained. This does not claim to sanitize every application log sink or HTTP error response. Upstream CI still requires maintainer approval. I am following the remaining PR checks; nothing has been merged. |
|
@Raghav-Bansal-15 Your revised authentication logging change preserves the useful full credential redaction, passkey URL coverage and safe diagnostic-code allowlist, with earlier contributor/reviewer authorship retained. I independently ran 161 Deno tests and 99 focused Kotlin/JUnit sanitizer tests, all passing. These include real production sanitizer/facade code and captured synthetic passkey diagnostics. The revised patch applies cleanly to freshly fetched dev The latest applicable Claude findings were checked against the current code and previous handoff. The URL-field and session-name fixes remain present; the finite diagnostic allowlist remains preferable to accepting arbitrary credential-shaped strings. Correlation IDs and generic HTTP-error-body sanitization remain explicitly outside this change's claims. #691's browser URL call sites and #640's userinfo sanitizer are complementary, not duplicates. I approved upstream run 34867321442 once. Upstream code-quality, shell, version and BOSS AI contracts checks pass; platform, compatibility and database checks are still incomplete at this checkpoint; fork CI and focused local tests do not substitute for those checks. Under the current bounded-review policy I am placing this in draft as a validation handoff, not for an author defect. To return it to ready: let current-head upstream checks finish, investigate any actual failures, then refresh dev integration and merge only after the required green checks. Do not repeat already resolved redaction findings or request another Claude review solely for the queue. No application was launched and desktop/mobile ceremony log inspection was not performed. No live secrets or database mutations were used. The original rating and attribution are unchanged. |
|
@claude Please review the current head of #567 against dev, including contributor follow-ups and prior finding dispositions. This is the current morning batch review; do not repeat repaired findings. Focus on remaining correctness, credential exposure, regressions and focused test coverage. Earlier reviewer repairs remain separately attributed. |
|
Claude finished @kshivang's task in 6m 35s —— View job Morning batch review: #567 current head (
|
| # | Status at HEAD |
|---|---|
| 1 URL params | challenge/email/credentialId/credential_id/sessionId/session_id are in sensitiveUriParamNames (LogSanitizer.kt:337-342), matching the real params built at DesktopPasskeyService.kt:75-80/:159-164. Pinned by passkeyPageParametersDoNotExposeCredentials. |
| 2 correlation IDs | PR body corrected; deferral is now stated rather than implied. |
| 3 allowlist | Expanded to 16 codes with "unlisted" for rejects. Shape matcher declined with a stated reason. See B below - the mechanism is right but unreachable at five call sites. |
| 4 response bodies | Deferred with concrete locations named. |
5 nameMarksSecret |
Special case replaced by the zipWithNext pair rule (:443-445), and sanitizeMap now also calls nameMarksSecret (:415) - so session-id, session.id, crossDeviceSessionId are covered on both paths. Pinned by prefixedAndSeparatedSessionNamesAreRedacted. Additive only: every name the pair rule catches was already caught by the substring test or was previously uncovered. |
Four findings remain, two of which are new consequences of the reviewer commit rather than leftovers.
A. authFailureDetails logs failed: false on the failure branch it was called from
utils/logging.ts:6 sets failed: error != null. Four call sites sit behind a guard with a second disjunct, so the branch fires with error nullish:
// utils/authorization.ts:81
if (error || !data?.user?.id) {
console.error('❌ Caller token rejected:', authFailureDetails(error))A token that resolves without an error but yields no user.id now logs ❌ Caller token rejected: { failed: false } - a line that denies the rejection it is reporting. This one is a strict regression: the replaced expression was error?.message ?? 'no user for token', and that fallback string was written deliberately to name exactly this branch. It is not credential material and nothing required its removal.
Same shape, lower stakes, at services/auth.ts:326 (!userResult.success || !userResult.user) and services/mobile.ts:30/:99/:131. And the two success lines - utils/database.ts:50 and services/auth.ts:482 - print error: {failed:false} on every successful query, which is the smaller point raised last round and still open.
Returning undefined for a nullish error fixes the success lines; the guarded branches additionally want a caller-supplied reason, since only the caller knows the second disjunct fired.
B. Five call sites hand authFailureDetails a string, so the expanded allowlist cannot fire there
The 16-code allowlist added in 771278f85 is the right mechanism, but at the sites the original review actually named it has nothing to read. These five pass a ServiceResult.error:
services/auth.ts:71←getUserPasskeysreturnserror: error.message(database.ts:431)services/auth.ts:327,:538←getUserWithEmailreturnserror: userError?.message || 'User email not found'(database.ts:547)services/auth.ts:372←storeCompletedAuthenticationreturnserror: error.message || error.code(database.ts:169)services/auth.ts:519←claimStoredSessionreturnserror: error.message(database.ts:204)
authFailureDetails bails at typeof error !== "object" (logging.ts:7), so all five can only ever emit {failed:true}. ❌ Failed to store completed authentication: {failed:true} - the exact line the first review picked out - is unchanged by the allowlist expansion, because the PostgrestError was already flattened to its message one frame down.
This is invisible to the new test: production challenge failures retain known codes drives storeChallenge, which logs the raw error object at challenge.ts:57, so it exercises the one shape that works. The fix is in the helpers - return code beside error, or hand the original error down - not in logging.ts.
Worth noting the direction of the loss at :372: storeResult.error is dropped from the log and then interpolated into the HTTP body two lines later (services/auth.ts:375). That is finding 4's deferral, but it means the string is present and readable at that call site while the log refuses it.
C. Seven full email addresses are still logged server-side - and 3d00f6d sharpened the asymmetry
The reviewer commit added email to sensitiveUriParamNames, so the desktop now refuses to log the email address it puts into the passkey URL. The edge function serving that URL logs the same address in clear, on the request that URL makes:
services/auth.ts:51 'Generating authentication challenge for email:', email
services/auth.ts:57 'User not found with email:', email
services/auth.ts:548 'Generating Supabase session for passkey auth:', userResult.user.email
services/mobile.ts:19 'Generating mobile registration page for:', email
services/mobile.ts:88 'Generating mobile authentication page for:', email
utils/database.ts:492 'Finding user by email:', email
utils/database.ts:507 'User not found with email:', email
utils/database.ts:551 'Found user email:', userData.email
The host has maskEmail for this and now applies it via the URL path. The PR body dispositions userId as "harmless identifiers remain intact" - which is arguable for an opaque UUID and not for an email address, and the two halves of the diff now state opposite policies for the same value. Either mask them or say in one sentence that server-side email is a deliberate retention, the way userId is.
D. The rule the PR wrote down is not applied to the sibling file in the same ceremony
CrossDeviceBrowserManager.kt:18 now carries the rule - "Keep raw throwables out of auth logs: their messages can contain the credential URL" - and it is accurate: java.net.URI.create(url) at :108 throws IllegalArgumentException("Illegal character in query at index N: <full URL>"). But DesktopPasskeyService, in the same flow, still logs raw throwables either side of it:
:105logger.error(LogCategory.PASSKEY, "Browser registration error", error = e):211logger.error(LogCategory.PASSKEY, "Biometric authentication error", error = e):107/:213PasskeyState.Error(e.message ?: …)puts the raw message into UI state
I traced the reachable exceptions: today the URL-bearing IllegalArgumentException is caught at CrossDeviceBrowserManager:121 and the fallback's IOException (Cannot run program "xdg-open"…) is what actually propagates, so there is no live leak. That is an accident of which frame catches, not a property - and BossLogger.writeToFileAsync appends error.message plus frames with no sanitizer, which is the follow-up the last round asked for. Applying sanitizeExceptionMessage(e.message) at these two sites costs nothing and makes the file consistent with the rule the PR just documented next door.
Smaller points
knownCodesis rebuilt on every call (logging.ts:9). It is a 16-elementSetallocated inside an error-path function reached from ~50 sites. Hoist it to a module-levelconst.withGenericErrorHandler:52substitutes before it redacts.error instanceof Error ? error : new Error("Unknown service failure")is an improvement overerror as Error, but the non-Errorcase is precisely the shape - a thrownPostgrestError-like object literal - whosecode/statusauthFailureDetailscould have kept. Pass the original toauthFailureDetailsand keep the substitution only foronError.- Import above the module docblock, still.
index.ts:1,utils/authorization.ts:1,utils/crypto.ts:1,utils/error-handler.ts:1each now open with theauthFailureDetailsimport and then the/** … */header, which detaches each file's rationale docblock from its file. Unaddressed from the last round. utils/logging.tsuses semicolons and double quotes where every neighbouring file uses neither;deno lintiscontinue-on-errorinedge-functions.yml, so nothing will flag it.CrossDeviceBrowserManagercall-site formatting is now mixed: three sites use positional args on one line, four usecategory =/message =/data =withdata =split onto its own line above a shortmapOf(...). Both are ktlint-clean; picking one would read better.- Pre-existing, in a block this PR reformatted: the inner
catch (fallbackException)atCrossDeviceBrowserManager:129-136is unreachable.openBrowserWithProcessBuildercatches internally and returns aResult, so it never throws - "All browser opening methods failed" cannot log, and the caller receives the exception logged at:201instead. Not introduced here; worth knowing before anyone relies on that line.
Regression coverage
Right, and better than last round. production challenge failures retain known codes and omit arbitrary codes drives a real storeChallenge failure and covers "unlisted" - which is what the previous test-authored console.error could not do. The canary-absent design in capture (including the 20-char prefix check) is the correct shape, and assertEquals(replay.refreshToken, refreshToken) proving redaction did not reach the response is still the best line in the file. CredentialRedactionTest closes the prefixed/separated session-name gap the last round identified, and plugin-logging's publish task depends on its own desktopTest, so these are on the gated path.
Remaining gaps, in order of what a test added now would actually catch:
- Nothing pins
{failed:false}(finding A) or the string-error collapse (finding B). Both are behaviours an operator will read as "no failure" / "no code available", and both would fail a test written today. verifyCallerTokenis untested, and it holds the one call site where an explicit diagnostic was removed.- Nothing pins the Kotlin call sites - only
LogSanitizeritself. The regression class this PR exists to prevent is a future edit restoringmapOf("url" to url)atCrossDeviceBrowserManager:101, and the sanitizer tests pass either way. The Deno half does test through production call sites; the Kotlin half does not. register/mobile?...&rpName=is not covered, onlyauth/mobile. Same param names, so low value.
I could not execute either suite here - deno is unavailable and Gradle/network calls are denied in this sandbox - so the PR body's pass claims are unverified by me. Everything above is from reading and hand-tracing HEAD; the flipped LogSanitizerTest expectations all agree with the new maskToken/maskSessionId implementations.
• branch fix/security-credential-logging
|
@Raghav-Bansal-15 Your current authentication redaction and prior contributor/reviewer work remain intact. I verified the actual auth-only diff and the earlier finding dispositions, and independently ran 161 passkey Deno tests plus 84 production Kotlin sanitizer/facade tests, all passing. A combined tree with #640 and #691 also passes 123 focused tests. No new in-scope code defect is established. The latest dev integration has one AGENTS.md conflict; I prepared that integration locally, keeping dev's documentation and preserving all original commits. The resulting diff is the 21 intended auth/logging files. That integration push is awaiting reviewer-side authorization, so the published head is still 56ca7f8 and has no current upstream check results. The once-requested current Claude review has completed; its dispositions follow. The shared local Gradle request was stopped while still waiting for the lock; no full local Gradle pass is claimed. This is an integration/authorization/validation checkpoint, not a request to repeat repaired work or an author defect. I am leaving the PR ready rather than drafting it for pending infrastructure. Preserve the finite error-code allowlist, passkey field/session-name regressions and explicit exclusion of generic HTTP-error response sanitization. Manual synthetic desktop/mobile auth-log checks remain unperformed. Current Claude response: the missing-user failed:false diagnostic, completed-auth storage code loss, and thrown-object diagnostic loss are reproduced with three failing Deno tests and fixed locally. The full suite now passes 164/164. Nullish errors omit the misleading failed flag; missing caller identity gets a fixed no_user_for_token reason; storage failures log allowlisted diagnostics before conversion to strings; generic wrappers preserve the original object for logging while keeping the safe callback fallback. These are reviewer repairs, not new contributor credit. Finding B is narrower than suggested: getUserPasskeys, getUserWithEmail and claimStoredSession already log the raw error through authFailureDetails at the database boundary, so their upper-layer string messages do not lose the only copy of the safe code. The actual missing storeCompletedAuthentication log is now covered, including its insert fallback. No shared ServiceResult redesign was needed. Server-side email and opaque user identifiers deliberately remain retained in this credential-capability scope; masking email in passkey URLs does not claim server-wide identity redaction. The sibling DesktopPasskeyService throwable observation explicitly found no reachable URL leak, so no speculative rewrite was added. Generic HTTP response sanitization remains deferred. Set-allocation, import placement and positional formatting nits are not merge-blocking in this bounded pass. No repeated Claude review requested. |
|
Follow-up at Original and reviewer commits are preserved. Remaining current-head hosted checks and upstream approval are separate gates; no merge or release. |
|
@Raghav-Bansal-15 I reviewed your follow-up e37c183 against current dev and the prior Claude dispositions. The missing-user reason, nullish-error flag, completion-store diagnostics on both paths, and thrown-object diagnostics are correctly repaired. Your replacement regressions cover those actual production paths. The updated sanitizer also preserves already-merged #640; no unrelated implementation is being counted as new work. Fresh independent checks pass: 164 Deno tests and 110 production Kotlin sanitizer/facade tests. Diff checks and current-dev merge-tree are clean. Exact-head fork Build & Test run 34977361361 is successful. No additional source repair or repeated Claude review loop is needed. Upstream run 34977121692 remains action_required. Automatic approval review rejected my request to approve that gated workflow, so explicit approval has been requested and I will not retry without it. This is reviewer-side authorization, not a failing test or author defect. I am leaving the PR ready; once upstream checks can run and pass, refresh dev/head/checks under the merge lock. The old request to push my local integration is superseded by your published follow-up and is no longer needed. Original 7.5/10 and reviewer/contributor attribution remain preserved. |
Rebase the reviewed 21-file authentication redaction patch onto the current dev tip (bc9aa3b). The previous integration commit was built on a pre-history-replacement snapshot that predates live-sessions, the terminal_sessions migrations, release notes and risa-labs-inc#769's enumeration-safe /auth/challenge work; merging the branch as published would have deleted those files. Only real content conflict: services/auth.ts error logs, where the masked authFailureDetails() logging (this PR) is retained over the raw error object that risa-labs-inc#769 re-introduced on the dev side. The merged effective diff against dev is byte-identical to the reviewed head's auth patch.
|
@claude Please review the current head b53379d (re-integrated onto dev tip bc9aa3b after the history replacement): verify the 21-file authentication redaction patch survived integration byte-identical, that services/auth.ts keeps masked authFailureDetails logging alongside #769's enumeration-safe challenge flow, and that nothing in the auth logging path still exposes tokens, session ids, emails or raw error payloads. |
|
Claude finished @kshivang's task in 5m 55s —— View job Review: #567 at
|
| Status | |
|---|---|
| #769 enumeration-safe challenge | Intact - inertChallenge ×4, rateLimit/429 in routes/auth.ts, auth.ts diff is logging-only |
| #691 query/fragment URL secrets, #640 userinfo, MCP auth-scheme redaction | Intact - LogSanitizer.kt's diff shows only maskToken/maskSessionId/the two name lists/nameMarksSecret; every other line matches dev |
Host LogSanitizer facade |
Pure delegation (utils/logging/LogSanitizer.kt:13-39), so BossAppWithAuth.kt's calls get the new behaviour - worth confirming because the host imports ai.rever.boss.utils.logging, not the plugin-logging object |
| URL param list vs the real passkey URL | challenge/email/credentialId/credential_id/sessionId/session_id all present, matching DesktopPasskeyService's built params; pinned by passkeyPageParametersDoNotExposeCredentials |
nameMarksSecret pair rule |
crossDeviceSessionId, session-id, session.id, SESSION_ID, sessionid all covered on both the map and free-text paths; session_status=active still survives |
No collision from adding email to the URI list |
No existing test in QueryParameterSanitizerTest, BrowserUrlLogConventionTest, DescribeUriSecretShapesTest or MagicLinkPasteTest asserts an email param surviving |
Prior dispositions I re-checked and am not re-raising: the {failed:false} fix (authFailureDetails now returns {} for nullish, and authorization.ts:82 supplies no_user_for_token), storeCompletedAuthentication's diagnostics on both write paths (database.ts:163, :174), thrown-object preservation in withGenericErrorHandler, the finite allowlist over a shape matcher, server-side email/userId retention, and the knownCodes/import-placement/formatting nits.
Server-side emails are still logged in 7 places (services/auth.ts:82, :573; services/mobile.ts:19, :88; utils/database.ts:494, :509, :553) - unchanged, as dispositioned.
Verification limits
deno is not installed on this runner and Gradle/network calls are denied, so I executed neither suite; everything above is from reading and hand-tracing HEAD. I also could not fetch e37c1836, so the "byte-identical to the reviewed patch" claim is unverified by me - what I verified is the diff against dev, which is the stronger check for finding 1 and is how finding 1 surfaced. CI on this head: Edge Functions green, Build & Test still queued - and note that Edge Functions being green is not evidence about finding 1, since index.ts has no test coverage at all.
--- • branch fix/security-credential-logging
Addresses the Claude review on head b53379d: - index.ts reverted two of dev's hardened 500 responses (the unauthenticated /maintenance/cleanup handler and app.onError). Restore both to the generic 'Internal server error' body; keep the redacted console.error half. - Move the entrypoint app to app.ts (with a lazily created, testable service-role client in utils/client.ts) so tests drive the real handlers, matching the crash-report/organisation pattern. - storeChallenge returns an allowlisted code alongside the error; services/auth.ts logs authFailureDetails for the store failure and services/registration.ts reports the code instead of the raw database message. - Wrap the eight remaining raw 'Route error:' logs in authFailureDetails. - New tests/app.test.ts pins: cleanup 500 and global 500 bodies never carry diagnostics, the global handler still logs, and both challenge flows keep Postgres messages off the wire.
|
Thanks — all three findings are legitimate and are fixed at 1.
2. 3. Eight raw Validation at |
|
@claude review — new head 83f8095 addresses your three findings on b53379d (evidence in the preceding comment): the two hardened 500 bodies are restored, the entrypoint app moved to app.ts with entrypoint-level canary tests (tests/app.test.ts), storeChallenge returns an allowlisted code, and the eight raw Route-error logs are wrapped. Please re-verify the integration against origin/dev and confirm no new issues in 83f8095 itself. |
|
Claude finished @kshivang's task in 4m 34s —— View job Re-review: #567 at
|
| Prior finding | Status at 83f8095a |
|---|---|
1. index.ts reverting dev's 500s |
Fixed. Both bodies are { error: 'Internal server error' } (app.ts:67, :120), the redacted console.error('Global error:', authFailureDetails(err)) kept. |
2. auth.ts:117 raw storeResult.error |
Fixed, and fixed at the source: storeChallenge returns code: error.code ?? 'unknown' (challenge.ts:60, :67) and the log now matches line 106's shape. |
3. Eight raw Route error: logs |
Fixed, all eight (routes/auth.ts:114,201,258, management.ts:127,213,304, register.ts:113,221), plus the two clientDataJSON catches. |
Two things remain, both about the response side that this commit started fixing.
1. auth/complete still returns the raw Postgres message - the sibling of the leak this commit closed
services/auth.ts:400:
console.error('❌ Failed to store completed authentication:', authFailureDetails(storeResult.error))
return {
success: false,
error: `Failed to store authentication result: ${storeResult.error || 'Unknown error'}`
}storeCompletedAuthentication returns error: error.message || error.code (utils/database.ts:164, :171), so storeResult.error is the raw Postgres message. routes/auth.ts:195 hands it straight out as a 400 body.
This was finding 4's deferral last round, and I would have left it there - except that this commit deliberately un-deferred the identical case one file over ("While wiring the canary I found one adjacent instance of the same class"), and the one that stayed is the more exposed of the two:
register/challenge(fixed) sits behindrequireAuthenticatedCaller(routes/register.ts:93) - the SQLSTATE goes to a caller who already proved account ownership.auth/complete(not fixed) has no caller authentication at all; possession of a live challenge is the only gate.
The fix is the one already applied next door: give storeCompletedAuthentication a code alongside error the way storeChallenge now has one, and report Failed to store authentication result (23505). Both its return sites (database.ts:164 and :171) already log through authFailureDetails, so the code is in hand.
2. The new response path puts an unfiltered error code on the wire
services/registration.ts:69:
error: storeResult.code ? `Failed to store challenge (${storeResult.code})` : 'Failed to store challenge'storeResult.code is error.code ?? 'unknown' straight off the PostgREST error - it never passes the 16-entry allowlist. That inverts this PR's own stated policy in the one direction that matters: the allowlist exists because "arbitrary credential material can satisfy those shapes" (the reason a shape matcher was declined for logs), and here an unvetted code goes into an HTTP body, which is the more exposed sink.
Practically the risk is small - for a PostgrestError this is a SQLSTATE, and the route is authenticated - so this is about keeping one definition of "safe code" rather than about a live leak. authFailureDetails({ code: storeResult.code }).code is the whole fix, and it makes the wire strictly no more permissive than the log. Worth a line in the KDoc either way, since a reader of challenge.ts:58-59 ("code is what callers may surface") will reasonably assume it was already filtered.
Two smaller things in the same two lines: the storeResult.code ? ternary has a dead false branch (both failure returns always set code), and services/auth.ts:117 builds authFailureDetails({ code: storeResult.code, message: storeResult.error }) - handing the raw message into the helper that currently drops it. It is safe today and one edit to logging.ts away from not being; passing only { code } costs nothing.
3. Test veracity: one assertion in app.test.ts cannot fail
tests/app.test.ts:73:
assert(!output.includes(diagnostic), "the log is redacted exactly like the response body")authFailureDetails returns {failed, code?, status?} and never a message, for any input - so this passes whatever the handler does, including if it logged a different error entirely. It reads like the canary check that carries the test, and the load-bearing assertion is actually the body one at :65. Asserting the log's shape (output.includes('"failed":true'), or that no code is emitted for a plain Error) would pin something.
The other three tests are good, and registration challenge store failure reports a code, never the database message is the right shape: it asserts the exact string Failed to store challenge (23505), so the fix above would surface there rather than silently.
One coupling worth a comment: test 2's mechanism depends on productionClient in utils/client.ts still being null when it runs, since the thrown Deno.env.get only fires on first creation. deno test shares module instances across files in a process, so a future test that drives app.request() without setPasskeyClientForTests would cache the client and this test would start failing for a reason unrelated to what it tests. It fails loudly (the 500 assertion), not silently, so this is a note rather than a defect - a resetPasskeyClientForTests() or the centralised teardown organisation/tests/helpers/mocks.ts uses would remove the ordering dependency.
Smaller points
- The move was not quite mechanical.
app.tschanges two published OpenAPI strings:"WebAuthn/Passkey authentication API for BOSS application"→"WebAuthn authentication API…"(:77) and"Passkey registration endpoints"→"WebAuthn registration endpoints"(:96). Unrelated to redaction and visible at/passkey/openapi. index.ts's 14-route inventory docblock was dropped, not moved. It was the only place the function's surface was listed in one view;app.ts's new header does not replace it.tests/routes.test.tsstill builds its own twin (buildApp,:19-30). That was unavoidable before; now thatapp.tsis importable, the twin is the thing that hid finding 1 last round and could be retired.knownCodesis still rebuilt per call (logging.ts:9) - a 16-elementSetallocated inside an error-path function reached from ~50 sites. Carried over, still one line.- Pre-existing, now that the body is fixed:
/passkey/maintenance/cleanupremains unauthenticated. It only deletes rows withexpires_at < now(), so this is an observation rather than a finding - but it is the endpoint the new test drives, so worth knowing it is reachable by anyone. - Import-above-the-docblock persists in
authorization.ts:1,crypto.ts:1,error-handler.ts:1(now notindex.ts, which was rewritten);utils/logging.tsstill uses semicolons and double quotes against the local style, anddeno lintiscontinue-on-error.
Verification limits
deno is not installed on this runner and Gradle/network calls are denied, so I executed neither suite - everything above is reading and hand-tracing 83f8095a. I did verify the claimed convention rather than take it: organisation/index.ts carries the byte-for-byte equivalent docblock and setServiceClientForTests, and crash-report/ has the same app.ts/index.ts pair. CI on this head: ⚡ Edge Functions green, 🔨 Build & Test queued.
--- • branch fix/security-credential-logging
…esponses Claude re-review follow-ups at 83f8095: - storeCompletedAuthentication now returns a code alongside error (like storeChallenge), and auth/complete's 400 body reports 'Failed to store authentication result (<code>)' through the authFailureDetails allowlist instead of the raw Postgres message. auth/complete is unauthenticated, so this is the more exposed of the two store-failure sinks. - register/challenge runs its code through the same allowlist (authFailureDetails({ code }).code) so log and wire share one definition of 'safe code'; the dead false branch of the old ternary is gone, and services/auth.ts logs store failures by code only rather than handing the raw message into the helper. - tests/app.test.ts: the global-handler test now asserts the structured log shape ('failed':true) instead of an assertion that cannot fail, drives the lazy client through resetPasskeyClientForTests so it no longer depends on module-instance ordering, and gains a canary test that drives a real signed assertion through completeAuthentication to a failing storeCompletedAuthentication and asserts the 23503 code reaches the error while the database message never does. - app.ts restores the two published OpenAPI strings dropped in the entrypoint rewrite; index.ts gets the 14-route inventory docblock back.
|
@claude re-review fixes are in at 1. auth/complete raw message — fixed. 2. Unfiltered code on the wire — fixed. 3. Test veracity — fixed. The global-handler test now asserts Smaller points: both published OpenAPI strings restored in |
|
Claude finished @kshivang's task in 7m 37s —— View job Re-review: #567 at
|
| Prior finding | Status at aae298c1 |
|---|---|
1. auth/complete raw message |
Fixed. storeCompletedAuthentication returns code on both failure returns (database.ts:164, :171), auth.ts:406 reports Failed to store authentication result (<code>) through authFailureDetails({ code }).code, log at :399 is code-only. |
| 2. Unfiltered code on the wire | Fixed. registration.ts:73 runs the code through the same allowlist; dead ternary branch gone; auth.ts:119 no longer hands the raw message into the helper. |
| 3. Test veracity | Fixed. '"failed":true' shape assertion added, and resetPasskeyClientForTests() (client.ts:34) removes the module-cache ordering dependency structurally. |
Smaller points also confirmed: both OpenAPI strings restored (app.ts:77, :96), the 14-route inventory is back on index.ts, and the two hardened 500s are intact (app.ts:67, :120).
The remaining issue is that the class of defect finding 1 belongs to is still open at three sites in two of the files this PR touched, and one of them is the same unauthenticated shape.
1. register/complete still returns the raw Postgres message - twice, and it is the sibling of the endpoint just fixed
routes/register.ts:216 returns ctx.json({ error: result.error || 'Registration failed' }, 400), and two paths put an unfiltered database string into result.error:
// services/registration.ts:273 ← storePasskeyInDB
error: storeResult.error || 'Failed to store passkey'
// database.ts:309 / :316 return { success: false, error: error.message }
// database.ts:323 return { success: false, error: (error as Error).message }
// services/registration.ts:167 ← verifyAndConsumeChallenge
error: challengeResult.error || 'Invalid challenge'
// database.ts:243 → consumeChallengeRow:90 return { consumed: false, error: error.message }
// database.ts:250 return { success: false, error: (error as Error).message }Three things line this up exactly with the case aae298c1 just closed:
- The exposure tier is the same.
routes/register.ts:204usesresolveOptionalCaller, notrequireAuthenticatedCaller- "a bearer token is honoured when the transport can carry one", per its own comment - soregister/completeis reachable with no caller authentication, likeauth/complete. The(code)reasoning written intoauth.ts:401-405applies verbatim. - The same inversion is present. All five source sites log through
authFailureDetails(database.ts:89,:249,:308,:315,:322) and then return the raw message to the caller - the log is redacted and the body is not. - It is in the file this commit half-fixed.
registration.ts:73now runs its code through the allowlist;:273, ~200 lines down in the same module, still shipserror.message.
storePasskeyInDB needs the code plumbing storeChallenge and storeCompletedAuthentication now have; consumeChallengeRow already has the code in hand at :89.
2. manage/list returns the raw message - the only one of the three management endpoints that does
services/management.ts:18:
error: result.error || 'Failed to fetch passkeys' // ← getUserPasskeys, database.ts:433 / :440manage/delete and manage/update in the same file already return 'Failed to delete passkey' / 'Failed to update passkey' after logging through authFailureDetails (:72, :123), so list is out of step with its own neighbours. Behind requireAuthenticatedCaller (routes/management.ts:34), so this is the lower tier - the same tier as register/challenge, which this branch did fix, which is why it is worth closing rather than deferring alongside it.
Smaller points
challenge.ts:58-59's comment is now misleading, and in the direction that causes this bug. "erroris safe for logs (already filtered upstream);codeis what callers may surface" -erroriserror.message, raw and unfiltered; what is filtered is theconsole.erroron the line above. A reader trusting that sentence would passstoreResult.errorto a log, which is precisely the call this commit removed atauth.ts:119. Rewording it to say "erroris the raw database message: server-side only, never logged and never returned" would make the comment match whatauth.tsnow does.'unknown'becomes'unlisted'on the wire. Both helpers normalise a missing code to the literal'unknown'(challenge.ts:60/:67,database.ts:164/:171), which is not inknownCodes, soauthFailureDetailsrewrites it to"unlisted"- a body readingFailed to store challenge (unlisted)where "unknown" was the accurate word, and it makes the?? 'unknown'tail atregistration.ts:73/auth.ts:406unreachable. Returningcode: error.code(undefined when absent) instead of?? 'unknown'gets both right and keepsunlistedmeaning whatlogging.ts:12says it means.captureLogs'sJSON.stringifymakes a quoted canary un-assertable.output.includes(canary)runs against JSON text, so a canary containing"or\is escaped in the haystack and can never match - the assertion passes no matter what was logged. Today it happens not to bite (CHALLENGE_CANARYandCOMPLETE_CANARYhave no quotes;CLEANUP_CANARYdoes, but is only asserted against the rawbody), so this is a trap for the next canary rather than a live gap.output.map(args => args.map(String).join(' ')).join('\n')removes it.- The new completion test could pin the positive side too.
!output.includes(COMPLETE_CANARY)is a regression guard rather than proof, for the same structural reason the test-2 assertion was replaced:authFailureDetailscannot emit a message for any input.assert(output.includes('23503'))is available here -authFailureDetails({code:'23503'})emits it - and would make the log half say something, matching what:119in the challenge test already does. - Mixed teardown convention: tests 1, 3 and 4 use
setPasskeyClientForTests(null)while test 2 usesresetPasskeyClientForTests(). Harmless (the others never create a production client), but now that the stronger verb exists it may as well be the only one. - Carried and explicitly skipped as optional, no argument from me:
knownCodesrebuilt per call (logging.ts:9), theroutes.test.tstwin, the unauthenticated/maintenance/cleanupas a follow-up issue. Also still carried: import-above-the-docblock inutils/authorization.ts:1,utils/crypto.ts:1,utils/error-handler.ts:1, andutils/logging.ts's semicolons/double quotes against the local style.
Integration state
git diff bc9aa3b2 HEAD is exactly 25 files, +820/-263, all auth/logging - matching the description, so the patch is intact against the dev tip it was integrated onto, and 83f8095a..HEAD touches no Kotlin.
origin/dev has since advanced to e9208143. git diff origin/dev HEAD shows six extra files this branch is simply behind on - BossTopBar.kt, DesktopGitService.kt, GitProviderWritesToRepoTest.kt, modules/boss-app-browser/build.gradle.kts, BrowserServiceImpl.kt, BrowserServiceImplTest.kt, which is #931's browser scheme gate and its neighbours. The next integration has to take dev's side on all six; replaying this branch's older copies is the exact mechanism that produced last round's finding 1. No passkey file and no LogSanitizer.kt moved in dev (the per-file counts are identical against both baselines), so the auth patch itself has no conflict surface this time.
Verification limits
deno is not installed on this runner and Gradle/network calls are denied, so I executed neither suite - the 179/179 claim is unverified by me and everything above is reading and hand-tracing aae298c1. I did check the mock harness rather than assume it: createMockSupabaseClient routes .upsert() onto the 'insert' queue (tests/helpers/mocks.ts:300-305) and mockAuthUser exists, so the new completion test's mocks line up with the production calls. Setting PASSKEY_RP_ID at module top level matches auth.test.ts:12 and e2e.test.ts:21, so it is the established convention here, not new coupling. CI on this head: ⚡ Edge Functions green, 🔨 Build & Test queued.
• branch fix/security-credential-logging
|
Thanks @Raghav-Bansal-15 for this PR. Before it can be reviewed or merged, it needs a rebase: against Could you rebase onto current |
|
Thanks for this, @Raghav-Bansal-15 - the change itself ("fix(auth): keep credentials out of authentication logs") looks worthwhile and we'd love to land it. One thing blocking review: this branch currently carries 91 commits, most of them unrelated work from other branches, so the PR diff shows far more than its own change and can't be merged as-is. Could you rebase onto the current |
08da448 to
4077231
Compare
|
@claude Preliminary triage re-review of #567 at exact head 6b9707a. This branch carries unrelated stale-dev history; isolate the credential-log hardening feature and subsequent focused fixes (the last reviewed fix was aae298c), NOT the giant GitHub PR diff. Inspect whether all credential-bearing auth response/log paths are covered and whether the security fix is already in current dev; list blockers and a viability/close recommendation. Triage only, not a merge verdict; full gate after rebase. |
|
Claude finished @fluck-boss's task in 4m 33s —— View job Preliminary triage: #567 at
|
| Branch change | State in dev |
|---|---|
BossAppWithAuth.kt URI + sessionId masking |
Already landed, and better. dev:53/:138 use LogSanitizer.describeUri(uri) (drops the query entirely) and :63/:73/:85/:94 use maskSessionId(link.sessionId) off a parsed link. The branch's maskUriParams + Regex("sessionId=([^&]+)") is the weaker version of both. |
CrossDeviceBrowserManager sessionId log |
Already landed (dev:148). |
email, sessionid in sensitiveUriParamNames |
Already landed, with a comment naming the passkey ceremony. |
| Server-side email / user-id masking (my earlier finding C) | Closed independently in dev by a different supabase/functions/passkey/utils/logging.ts exporting maskEmail/maskUserId/maskPasskeyId/maskSessionId, with 5 production importers and its own tests/logging.test.ts. |
/maintenance/cleanup exposure (my "observation, worth a follow-up") |
Closed in dev - extracted to routes/maintenance.ts and gated on the service-role key, failing closed when it is unset. |
What is not in dev (still genuine, and the reason to keep this)
authFailureDetailsdoes not exist indevat all (zero grep hits).devstill has ~40console.error(..., error)sites handing rawPostgrestError/GoTrue objects to persistent logs -utils/database.ts×12,utils/challenge.ts×4,routes/*.ts×8,utils/jwt.ts,utils/crypto.ts,utils/error-handler.ts.- The one concrete live leak on
devright now:CrossDeviceBrowserManagerdev:92anddev:132logmapOf("url" to url)raw, and that URL is the passkey ceremony URL carryingchallenge=,email=,sessionId=,credentialId=.dev'sBossLoggerstill has no sanitizer on thedatamap (noLogSanitizerreference in it outside the KDoc), so this reaches the log file verbatim. This PR's two masked call sites are the fix and nothing else ondevcovers them. challenge,credentialId,credential_idare still missing fromdev'ssensitiveUriParamNames- so evendev's ownmaskUriParamswould leak the challenge if someone wired it up at those two sites. The Kotlin half of this PR is therefore both halves: the call sites and the vocabulary.nameMarksSecret's session adjacent-word rule.- The response-body
(code)hardening inservices/auth.ts/services/registration.ts-devstill returns the raw Postgres message (dev:auth.ts:400,dev:registration.ts:68,:270). app.ts/client.tsentrypoint split andtests/app.test.ts's canary tests.
Contested - a policy decision, not a merge
maskToken → constant [REDACTED] and maskSessionId → maskToken. dev's newer utils/logging.ts docblock states the opposite policy in as many words: "The masked forms keep just enough of the value to correlate two lines from one request without storing the identifier." Landing this as-is leaves the host saying "no fragment, ever" while the edge function it talks to deliberately keeps a 4-char prefix - and my first review's point 2 (no correlation id was ever added to replace what the mask dropped) is still unaddressed by design. Someone has to pick one policy for both halves; that is not something a rebase resolves.
Blockers
app.tsrevertsdev's service-role gate on/maintenance/cleanup. The branch'sapp.ts:58inlinesapp.post("/maintenance/cleanup", …)whiledevdeleted that inline route and replaced it with an authenticatedroutes/maintenance.ts. Replaying the branch removes a landed authentication check on an unauthenticated scan-and-delete primitive. Same mechanism as theindex.tsrevert I flagged atb53379dd, worse consequence. Also breakstests/app.test.ts, which POSTs that path unauthenticated and asserts 500 - ondevit is 401.utils/logging.tsis an add/add collision with disjoint content. Branch =authFailureDetails;dev= the masking module with 5 importers. "Take ours" deletesdev's masking and breaksservices/auth.ts,services/management.ts,services/mobile.ts,services/registration.ts,tests/auth.test.ts. The two need to coexist in one file (orauthFailureDetailsneeds its own module name).tests/logging.test.tsis the same add/add collision.- Test deletions. The branch drops files
devadded:FileLogThresholdTest.kt(124),FileLoggingResolutionTest.kt(123),FreeTextUserInfoTest.kt(144),SupabaseKeyShapeTest.kt(80),tests/ceremony-abuse.test.ts(261), plus reverts insideauth.test.ts,mobile.test.ts,management.test.ts,registration.test.ts,rate-limit.test.ts,QueryParameterSanitizerTest.kt. BossLogger.ktreverted by 157 lines (dev's file-logging threshold/resolution work).- ~16 of the 25 feature files have dev-side changes since
bc9aa3b2, so this is not a two-or-three-hunk rebase.
Still-open inside the feature itself (carried from the aae298c1 review, unfixed)
Four raw database messages still reach HTTP bodies on this branch - and all four are still raw on dev too, so they are open either way:
services/auth.ts:210→routes/auth.ts:196400 onauth/complete(unauthenticated). This is the direct sibling of the caseaae298c1fixed twelve lines later at:406.services/registration.ts:167and:273→routes/register.ts400 onregister/complete(resolveOptionalCaller, so also reachable unauthenticated).services/management.ts:18→manage/list(authenticated; its two neighbours in the same file already return constants).
Viability / recommendation
Keep, don't close - but close this branch and re-cut. Rebasing 6b9707a2 is not viable: it is not a history-cleanup problem, it is that the branch and dev independently rewrote the same subsystem and the branch's copy is older on the points where they overlap. Any rebase that resolves conflicts by favouring the branch re-opens a fixed auth hole.
Suggested split, off fresh dev, smallest-first:
- A - Kotlin (small, highest value/line). Mask the two
CrossDeviceBrowserManagerURL sites (describeUriis the better fit and matches whatdev'sBossAppWithAuthalready chose), addchallenge/credentialId/credential_idtosensitiveUriParamNames, add thenameMarksSecretsession pair rule, and portCredentialRedactionTest. Drop theBossAppWithAuth.ktchanges entirely - superseded. This is a handful of lines and closes the one live leak. - B - Deno diagnostics.
authFailureDetailsadded todev's existingutils/logging.tsbeside the mask helpers (not replacing it), the ~50 call-site wraps, thecode-plumbing instoreChallenge/storeCompletedAuthentication, and the(code)response bodies - extended to cover the four sites above rather than the two already done. Rebuildtests/app.test.tsagainstdev'sroutes/maintenance.tsshape (401, not 500). Theapp.ts/client.tsentrypoint split is worth keeping -devstill has no test seam onindex.ts- but it must mountdev's maintenance router, not re-inline the route. - C - separate decision. The
maskToken/maskSessionIdfull-redaction policy, with whatever correlation mechanism replaces the dropped prefix, argued againstdev's stated "keep enough to correlate" policy.
A and B are each independently mergeable and neither depends on the other.
Verification limits
Shallow checkout with no merge base to origin/dev, so everything above is git diff against explicit endpoints plus reading dev's current file contents. deno is not installed on this runner and Gradle is denied, so I executed neither suite. No CI runs are reported against this head.
• branch fix/security-credential-logging
|
Thanks @Raghav-Bansal-15 for the credential-logging hardening. I triaged the focused work against current dev rather than the branch's much older history. There is real work here that dev still needs: |
072a2bd to
2ae2565
Compare
Stops credentials from reaching authentication logs.
What changed: token and session masks now write [REDACTED] instead of showing part of the value. Deep-link and browser logs mask URLs and session ids. The passkey edge function keeps the original storage error for server-side diagnosis through authFailureDetails, but returns only a safe message to callers. A passkey-lookup failure still returns the same inert challenge as other pre-auth failures, so error paths do not reveal whether an account exists.
Why: partial masks still leak credential material into logs, and raw storage errors reached responses. Server logs keep what operators need, clients get nothing sensitive.
How it was tested: 174 Deno passkey tests pass. Focused Kotlin suites for the sanitizer and redaction pass. ktlint and detekt pass on the touched modules. Full CI runs on this head.