fix(security): harden 2FA setup, OIDC default role, and rate-limit fallback (v0.17.3-beta)
Address the actionable findings from an adversarial review: - 2FA enrollment now requires the current password (/totp/setup), so a hijacked session or XSS can't enable TOTP and lock the real user out. SSO-only accounts (no local password) are told to set one first. Frontend collects the password before starting setup. - OIDC config rejects default_role='admin': auto-provisioning unmatched users straight to admin is almost never intended — admin must come from an explicit group → role mapping. - Rate limiter no longer fails fully open on a Redis outage. Redis calls are bounded by an 800ms timeout (the client uses maxRetriesPerRequest:null, so commands would otherwise hang, not throw), and on failure a per-process in-memory sliding-window fallback keeps /login and other limited endpoints throttled. Verified with Redis stopped: attempts throttle (429) instead of hanging or passing unlimited. Findings intentionally not actioned: the "API-token auth is fire-and-forget" report was a false positive (Express awaits next(), which is called inside the promise), and the webhook DB-pool and JWT-revocation items are either overstated or a separate architectural decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b3ba2b98dc
commit
28701cfc67
@@ -137,6 +137,7 @@ export default function SettingsPage() {
|
||||
// ─── TOTP / 2FA ────────────────────────────────────────────────────────────
|
||||
const [totpSetupData, setTotpSetupData] = useState<{ secret: string; uri: string; qr: string } | null>(null);
|
||||
const [totpConfirmCode, setTotpConfirmCode] = useState('');
|
||||
const [totpSetupPassword, setTotpSetupPassword] = useState('');
|
||||
const [totpDisablePassword, setTotpDisablePassword] = useState('');
|
||||
const [totpMsg, setTotpMsg] = useState('');
|
||||
const [totpError, setTotpError] = useState('');
|
||||
@@ -148,8 +149,8 @@ export default function SettingsPage() {
|
||||
});
|
||||
|
||||
const startTotpSetupMutation = useMutation({
|
||||
mutationFn: () => authApi.totpSetup(),
|
||||
onSuccess: (res) => { setTotpSetupData(res.data); setTotpConfirmCode(''); setTotpError(''); },
|
||||
mutationFn: () => authApi.totpSetup(totpSetupPassword),
|
||||
onSuccess: (res) => { setTotpSetupData(res.data); setTotpConfirmCode(''); setTotpSetupPassword(''); setTotpError(''); },
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error;
|
||||
setTotpError(msg || 'Failed to start TOTP setup');
|
||||
@@ -1157,14 +1158,26 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
) : (
|
||||
/* Not enabled, not in setup */
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="label">Confirm your password to set up 2FA</label>
|
||||
<input
|
||||
type="password"
|
||||
className="input max-w-xs"
|
||||
value={totpSetupPassword}
|
||||
onChange={(e) => setTotpSetupPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => startTotpSetupMutation.mutate()}
|
||||
disabled={startTotpSetupMutation.isPending}
|
||||
disabled={!totpSetupPassword || startTotpSetupMutation.isPending}
|
||||
className="btn-primary"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
Set up 2FA
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user