fix(auth): redact Clerk API key id from revocation error logs - #250
Open
detail-app[bot] wants to merge 1 commit into
Open
fix(auth): redact Clerk API key id from revocation error logs#250detail-app[bot] wants to merge 1 commit into
detail-app[bot] wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detail bug report: View on Detail
Closes #246
Bug
On a Clerk transport-layer failure (connection refused, DNS, timeout, TLS),
AuthenticatedUserController.revokeCurrentApiKeylogs the caughtApiKeyOperationUnavailableExceptionvialog.error(message, throwable). The exception's cause chain carries a SpringResourceAccessExceptionwhose message embeds the resolved revoke URI —https://api.clerk.com/v1/api_keys/ak_…/revoke— so theak_…key id renders into the CONSOLE log'sCaused by:line. The regression test added alongside thelog.errorcall (commitce8b434c) passed vacuously: it threw a causeless exception and asserted only ongetFormattedMessage()(the constant"Clerk API key revocation was unavailable"), which excludes throwable rendering, so it never exercised the leaking channel. The commit's ownassertFalse(...contains(CLERK_API_KEY_ID))assertion stated the id should not appear, yet it did.Fix
Trace-preserving Logback redaction (the bug report's recommended Option A):
RedactingThrowableProxyConverterextends Logback'sThrowableProxyConverterand overridesthrowableProxyToStringto replaceak_[A-Za-z0-9_]+withak_***on the rendered stack/cause chain. It is a genuine Logback pattern-converter SPI implementation (the*Convertersuffix is the framework's own naming forThrowableProxyConverter/ExtendedThrowableProxyConverter), not a data-reshaping adapter. The regex covers both the non-secret id (ak_<id>) and the secret token form (ak_secret_<material>).logback-spring.xmlregisters the converter as%redactedExand appends it to the CONSOLE pattern. Because the converter extendsThrowableHandlingConverter, Logback'sEnsureExceptionHandlingdetects it and does not also append a second, unredacted default throwable converter — so the stack trace renders exactly once with the id redacted (no re-leak), and the full exception type,Caused by:chain, frames, host, and path remain for operator triage.Rendered output after the fix:
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://api.clerk.com/v1/api_keys/ak_***/revoke": Connection refused— only theak_token is removed; diagnosability is intact. The controller and verifier code are left untouched; HTTP behavior is unchanged (still 503 with the same error body, still one ERROR event).Testing
RedactingThrowableProxyConverterTest) — four cases pinning the converter's durable contract: the id is redacted toak_***; the secret form is also redacted; the stack trace and fullCaused by:chain are preserved; andCaused by:appears exactly once (proves Logback doesn't double-append an unredacted copy once%redactedExis present).RevokeApiKeyIdLogLeakTest.renderedRevocationLogExcludesApiKeyId) — boots@SpringBootTestso the livelogback-spring.xmlCONSOLE pattern is under test, drivesDELETE /api/me/api-keywith a verifiedak_bearer whose mockedrevokethrows an exception built by running a realRestClientagainst a connection-refusing transport (so the cause is a genuine SpringResourceAccessExceptionwhose message embeds the revoke URI, exactly as in production). It asserts the rendered CONSOLE line excludes the id, containsak_***, and still contains the outer exception, theCaused by: ResourceAccessExceptionline, andConnection refused. This is the non-vacuous regression guard the original commit lacked — it asserts on the rendered line (notgetFormattedMessage()) and drives a production-shaped cause.Caused by: … ResourceAccessException: "…/api_keys/ak_0123456789abcdef0123456789abcdef/revoke"andexpected: <false> but was: <true>); restoring%redactedExmade it pass again. This was a manual probe, not a committed test.ClerkApiKeyVerifier.revokewitness confirming Spring embeds the id on the wrappingResourceAccessException, and a shared transport-failure stub) but excluded them from the commit — they assert the bug exists (a probe of the mechanism) rather than guard against a future regression, so per the repo's probe-vs-test policy they were deleted before committing; the production-shaped exception is now inlined in the acceptance test.Routine checks (all green):
./gradlew compileJava compileTestJava,spotlessCheck,pmdMain,pmdTest,spotbugsMain(with FindSecBugs),spotbugsTest,ast-grep scan src/main/java/(naming/type rules — no violation from the new*ConverterSPI class), the full unit suite (1168 tests, 0 failures, 0 errors, 1 pre-existing environment-dependent skip unrelated to this change), and the existingAuthenticatedUserEndpointSecurityIntegrationTest(9/9, including the previously-vacuous revocation test, still passing) andCustomErrorControllerTest(13/13, which renders through the same live CONSOLE encoder — confirms non-throwable events are unchanged).make lint-astrequiredast-grepandruby, which were missing from the sandbox; I installedast-grep0.45.3 vianpm install @ast-grep/cliandruby3.2.3 viaapt-get install -y ruby, after which the lane passed (3/3 ast-grep snapshot tests, scan clean, chat-model SSOT self-test + match OK).git diff --checkis clean.Automatic Fixes PRs can be configured here.