feat(web): reveal signing secrets after creation#90
Merged
Conversation
The signing-secret plaintext is already stored in the DB (the relay needs it to compute HMACs), so the previous "show once at creation" posture was UX friction for no security gain. ListSigningSecrets now returns the full secret alongside the prefix, and the settings page adds a Show/Hide toggle with copy-to-clipboard per row. API keys are deliberately not changed — they're stored as SHA-256 hashes today, and adding a plaintext column would be a real security regression. If a user loses an API key they should delete + recreate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the "save now, you can't see it again" copy on the post-create banner; with Show/Hide in the table that claim is no longer true. Banner now just announces the new secret and points at the table. - Update the stale Go doc on handleListSigningSecrets that still claimed plaintext was excluded from list responses. - Rename TestSigningSecrets_Create_ReturnsPlaintextOnce → ReturnsPlaintext, since the test already covers list-after-create. - Fix the test fixture math: "abcd1234efgh".repeat(5) is 60 chars, not 64. Bumping to repeat(6) so the slice actually matters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operational hardening for the signing-secret reveal endpoint:
- Audit log each List call with user_id + count. The endpoint now
hands out live HMAC credentials, so we want forensic breadcrumbs
("who pulled what when") without putting the values themselves in
the log.
- Set Cache-Control: no-store on the List response. The Authorization
header should already keep shared caches off it, but defense in
depth is cheap here.
Coverage gaps called out in review:
- TestSigningSecrets_List_DoesNotLeakOtherUsers: pins the most
load-bearing property of this endpoint — A's plaintext must never
appear in B's list response.
- TestSigningSecrets_List_SetsNoStore: locks in the header.
- Web: clipboard mock + Copy-button test asserting writeText is
called with the full plaintext and the label flips to "Copied".
Also tightened the SigningSecretSummary godoc — the prior wording
suggested API keys were moving away from hashes-only, which is the
opposite of the design intent.
Verified codegen round-trip: ran `make swagger`, `npm run
generate:openapi3 && generate:types`, and `datamodel-codegen` — all
three produced byte-for-byte identical output to the hand-edits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
GET /api/v1/users/me/signing-secretsnow returns the full plaintextsecretfield alongside the existingsecret_prefix. Plaintext was already stored inwebhook_signing_secrets.secret(the relay needs it to sign HMACs), so this is a behavioral relaxation only — no new attack surface vs. DB-level access.Test plan
go test ./internal/identity/... ./internal/agent/...(serial; project requires-p 1)cd web && npm test— 136/136 passing, including new Show/Hide testcd web && npm run lint