Skip to content

feat(presentation-mode): mask API keys and tokens for screensharing - #533

Closed
nicdavidson wants to merge 2 commits into
developfrom
feat/presentation-mode
Closed

nicdavidson wants to merge 2 commits into
developfrom
feat/presentation-mode

Conversation

@nicdavidson

Copy link
Copy Markdown
Contributor

Base note: this was intended to target develop, but develop no longer exists on the remote — it was merged into main in #532 and deleted. main is the only branch there now, and its content is identical to the old develop tip, so this targets main. Happy to retarget if develop gets recreated.

Why

Screensharing the admin UI puts live API keys on someone else's monitor. On a local instance the API Keys page renders 10 full keys as plain text, and /api-security/api-keys does the same.

One toggle in the top bar (next to the theme toggles) now masks every credential the UI renders, with a per-field eye to reveal one on demand.

Toggle off keys render as today
Toggle on every key and token becomes ••••••••••••
Per-field eye reveals one value; flipping the global toggle back on re-hides it
Persists stored in localStorage, so a demo doesn't start exposed

The constraint that shaped the design

Masked curl commands still have to paste into Postman and work.

Display and clipboard are therefore separate paths everywhere:

  • df-try-it renders displaySnippet, copies snippet
  • df-mcp-connect renders maskIfPresenting(x), copies x

Masking never touches the model. The curl command itself stays fully legible on screen — URL, method, headers — only the credential is covered.

The value also leaves the DOM rather than being blurred. A CSS blur stays selectable and recoverable from a screenshot; a test asserts the raw key is absent from innerHTML.

Service config

Secrets key off the field name, not the schema type, because the schema can't be trusted: oidc.client_secret ships as type text and mcp.oauth_client_secret as string, so both render as plain visible inputs today. _id is excluded so client_id stays readable during OAuth setup.

Surfaces covered

API-key table, app details, MCP access key, MCP connect snippets (×5), artifact-card auth header, celebration dialog, try-it curl/Python/JS/MCP snippets, and df-dynamic-field for service-config credentials.

Swagger's raw-spec panel is untouched and doesn't need to be: dfApiDocsApiKey is '' in both environment files, so its curl carries no real key.

Tests

npm run test:ci — 203 passed, up from 169. The 34 new specs are registered in jest.config.ci.js, so they actually gate.

e2e/presentation-mode.spec.ts — 9 passed against a live instance. The one that matters:

✓ curl snippets mask the credentials but copy the real command

It captures the real snippet, enables masking, asserts the credential is gone from screen, clicks Copy, and asserts clipboard === the original unmasked command. Postman-paste is pinned by exact equality, not by inspection.

Also covered: keys absent from innerHTML; the eye reveals without navigating away (table rows are clickable — the test caught the missing stopPropagation); setting survives reload; off restores the keys; and a data-driven sweep of the leaking routes that fails loudly if a swept route has no credential left to hide, so it can't rot into a no-op.

Regression check: full e2e is 32 passed / 5 failed. The 5 nav-overviews failures are pre-existing — verified by stashing this work and re-running on a clean tree for the identical result. They are an environment difference in the local instance, unrelated to this change.

Not a security boundary

The key still arrives in the API response and is visible in devtools. This addresses screenshare exposure, not exfiltration.

Screensharing the admin UI puts live API keys on someone else's monitor.
One toggle in the top bar now covers every credential the UI renders, with
a per-field eye to reveal one on demand.

Display and clipboard are deliberately separate paths: df-try-it renders
displaySnippet and copies snippet, df-mcp-connect renders
maskIfPresenting(x) and copies x. A masked curl command still pastes into
Postman and works.

The value leaves the DOM rather than being blurred — a CSS blur stays
selectable and recoverable from a screenshot.

Service-config secrets key off the field name, not the schema type:
oidc.client_secret ships as `text` and mcp.oauth_client_secret as
`string`, so both render as plain visible inputs. `_id` is excluded so
client_id stays readable during OAuth setup.

This is not a security boundary — the key still arrives in the API
response and is visible in devtools. It addresses screenshare exposure.

Covered by 34 unit tests (registered in jest.config.ci.js) and
e2e/presentation-mode.spec.ts, which asserts the clipboard still carries
the exact unmasked command and sweeps the leaking routes, failing loudly
if a swept route has no credential left to hide.
@nicdavidson

Copy link
Copy Markdown
Contributor Author

Superseded by #537, which carries these presentation-mode commits (plus a fix for unmasked keys wrapping in the API Keys table) rebased onto current main.

@nicdavidson nicdavidson closed this Oct 5, 2026
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.

1 participant