From 3b597ea028193e0ee2d8e934d26089782203b812 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 29 May 2026 18:38:17 +0200 Subject: [PATCH] agent: mask OAuth code input + reveal toggle on login screen (closes #568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login-in-progress screen's OAuth-code input was a plain text field — anyone shoulder-surfing or capturing a screenshot of the agent web UI would see the code in cleartext. Same risk applies to dashboard share-screens during live demos. Changes: - input switches to type='password' so the pasted code renders as bullets by default. Placeholder updated to '(hidden)' so the operator knows the masking is intentional, not a browser quirk. - new 'reveal' button (👁) next to the input flips the type back to text on press, so the operator can sanity-check the paste before submitting if she wants. aria-pressed reflects state. - CSS for the reveal button mirrors the existing .btn-login amber family — quiet by default, amber border/glow when pressed. - spellcheck='false' on the input so browsers don't try to underline the random-looking string as a typo. The on-screen OAuth URL stays visible (the operator needs to click it). The code is the secret leg — only the operator's browser holds it, the URL is what was posted publicly to claude's OAuth provider. --- frontend/packages/agent/src/agent.css | 32 ++++++++++++++++++++++++++- frontend/packages/agent/src/app.js | 30 +++++++++++++++++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index abd1ba1c..4243acf7 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -413,7 +413,7 @@ a:hover { color: var(--fg); text-shadow: 0 0 12px rgba(137, 220, 235, 0.9); } flex: 1; } .sendform input:focus { outline: 1px solid var(--purple); } -.loginform { display: flex; gap: 0.6em; margin-top: 0.5em; } +.loginform { display: flex; gap: 0.6em; margin-top: 0.5em; align-items: stretch; } .loginform input { font-family: inherit; font-size: 1em; background: rgba(255, 255, 255, 0.04); @@ -423,6 +423,36 @@ a:hover { color: var(--fg); text-shadow: 0 0 12px rgba(137, 220, 235, 0.9); } flex: 1; } .loginform input:focus { outline: 1px solid var(--purple); } + +/* #568: show / hide toggle for the masked OAuth-code input. Quiet + by default (muted border + transparent bg), lights amber on + hover / when pressed (aria-pressed="true") so the operator + sees at a glance whether the code is currently visible. */ +.loginform-reveal { + font-family: inherit; + font-size: 1em; + background: transparent; + color: var(--muted); + border: 1px solid var(--purple-dim); + border-radius: 3px; + padding: 0 0.6em; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} +.loginform-reveal:hover, +.loginform-reveal:focus-visible { + color: var(--amber); + border-color: var(--amber); + outline: none; +} +.loginform-reveal[aria-pressed="true"] { + color: var(--amber); + border-color: var(--amber); + box-shadow: 0 0 8px -2px var(--amber); +} pre.diff { background: rgba(255, 255, 255, 0.03); border: 1px solid var(--purple-dim); diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 87b12306..a6aa8c47 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -351,11 +351,33 @@ window.marked = marked; const code = el('form', { action: '/login/code', method: 'POST', class: 'loginform', 'data-async': '', }); + // #568: OAuth code is a sensitive secret — mask the input with + // type="password" so a shoulder-surfer / screenshot doesn't + // capture it. The reveal button flips it back to text on press + // so the operator can sanity-check the paste before submit. + const codeInput = el('input', { + name: 'code', + type: 'password', + placeholder: 'paste OAuth code here (hidden)', + required: '', + autocomplete: 'off', + spellcheck: 'false', + }); + const reveal = el('button', { + type: 'button', + class: 'loginform-reveal', + title: 'show / hide pasted code', + 'aria-label': 'show / hide pasted OAuth code', + 'aria-pressed': 'false', + }, '👁'); + reveal.addEventListener('click', () => { + const showing = codeInput.getAttribute('type') === 'text'; + codeInput.setAttribute('type', showing ? 'password' : 'text'); + reveal.setAttribute('aria-pressed', showing ? 'false' : 'true'); + }); code.append( - el('input', { - name: 'code', placeholder: 'paste OAuth code here', - required: '', autocomplete: 'off', - }), + codeInput, + reveal, el('button', { type: 'submit', class: 'btn btn-login' }, '◆ S3ND C0DE'), ); root.append(code);