agent: mask OAuth code input + reveal toggle on login screen (closes #568)
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.
This commit is contained in:
parent
5f8bc5315a
commit
3b597ea028
2 changed files with 57 additions and 5 deletions
|
|
@ -413,7 +413,7 @@ a:hover { color: var(--fg); text-shadow: 0 0 12px rgba(137, 220, 235, 0.9); }
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.sendform input:focus { outline: 1px solid var(--purple); }
|
.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 {
|
.loginform input {
|
||||||
font-family: inherit; font-size: 1em;
|
font-family: inherit; font-size: 1em;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
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;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.loginform input:focus { outline: 1px solid var(--purple); }
|
.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 {
|
pre.diff {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border: 1px solid var(--purple-dim);
|
border: 1px solid var(--purple-dim);
|
||||||
|
|
|
||||||
|
|
@ -351,11 +351,33 @@ window.marked = marked;
|
||||||
const code = el('form', {
|
const code = el('form', {
|
||||||
action: '/login/code', method: 'POST', class: 'loginform', 'data-async': '',
|
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(
|
code.append(
|
||||||
el('input', {
|
codeInput,
|
||||||
name: 'code', placeholder: 'paste OAuth code here',
|
reveal,
|
||||||
required: '', autocomplete: 'off',
|
|
||||||
}),
|
|
||||||
el('button', { type: 'submit', class: 'btn btn-login' }, '◆ S3ND C0DE'),
|
el('button', { type: 'submit', class: 'btn btn-login' }, '◆ S3ND C0DE'),
|
||||||
);
|
);
|
||||||
root.append(code);
|
root.append(code);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue