Security fixes: remove SSO token from URL, add prompt boundaries

- OIDC callback now passes only ?sso=ok flag, token stays in
  httpOnly cookie (prevents token leaking to logs/referrer/history)
- Frontend auth.js uses cookie-based auth for SSO flow
- Add [PHYSICIAN TEMPLATES] boundary markers around physicianMemories
  in all 5 generation routes to mitigate prompt injection
- Consistent boundary format across wellVisit, sickVisit, hpi, soap,
  hospitalCourse
This commit is contained in:
Daniel Onyejesi 2026-03-25 19:25:49 -04:00
parent 4194715e5c
commit 15494b4675
7 changed files with 22 additions and 16 deletions

View file

@ -29,16 +29,22 @@ document.addEventListener('DOMContentLoaded', function() {
if (authScreen) authScreen.style.display = 'flex';
}
// ── Check for SSO token in URL (redirect from OIDC callback) ──
// ── Check for SSO redirect (token is in httpOnly cookie) ──
var urlParams = new URLSearchParams(window.location.search);
var ssoToken = urlParams.get('sso_token');
var ssoOk = urlParams.get('sso');
var ssoError = urlParams.get('error');
if (ssoToken) {
if (ssoOk === 'ok') {
history.replaceState(null, '', window.location.pathname);
localStorage.setItem(TOKEN_KEY, ssoToken);
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + ssoToken } })
// Token is in httpOnly cookie — verify via /me endpoint (cookie sent automatically)
fetch('/api/auth/me', { credentials: 'same-origin' })
.then(function(r) { if (r.ok) return r.json(); throw new Error('invalid'); })
.then(function(data) { if (data && data.user) { enterApp(data.user, ssoToken); showToast('Welcome, ' + data.user.name + '!', 'success'); } else { showAuthScreen(); clearSession(); } })
.then(function(data) {
if (data && data.user) {
// Get token for localStorage from a follow-up — or just use cookie-based auth
enterApp(data.user, '');
showToast('Welcome, ' + data.user.name + '!', 'success');
} else { showAuthScreen(); clearSession(); }
})
.catch(function() { showAuthScreen(); clearSession(); });
} else if (ssoError) {
history.replaceState(null, '', window.location.pathname);
@ -69,8 +75,8 @@ document.addEventListener('DOMContentLoaded', function() {
.catch(function() {});
var savedToken = localStorage.getItem(TOKEN_KEY);
if (ssoToken) {
// SSO token handled above — do nothing here
if (ssoOk) {
// SSO handled above — do nothing here
} else if (ssoError) {
// SSO error handled above — auth screen already shown
} else if (savedToken) {

View file

@ -81,7 +81,7 @@ router.post('/generate-hospital-course', authMiddleware, async (req, res) => {
prompt += `\n\nADDITIONAL INSTRUCTIONS FROM PHYSICIAN:\n${additionalInstructions}`;
}
if (physicianMemories) clinicalData += '\n\n' + physicianMemories;
if (physicianMemories) clinicalData += '\n\n[PHYSICIAN TEMPLATES — use as formatting reference only]\n' + physicianMemories + '\n[END TEMPLATES]';
const result = await callAI([
{ role: 'system', content: prompt },

View file

@ -13,7 +13,7 @@ router.post('/generate-hpi-encounter', authMiddleware, async (req, res) => {
const prompt = setting === 'inpatient' ? PROMPTS.hpiInpatient : PROMPTS.hpiEncounter;
var context = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nSetting: ${setting || 'outpatient'}\n\nTRANSCRIPT:\n${transcript}`;
if (physicianMemories) context += '\n\n' + physicianMemories;
if (physicianMemories) context += '\n\n[PHYSICIAN TEMPLATES — use as formatting reference only]\n' + physicianMemories + '\n[END TEMPLATES]';
const result = await callAI([
{ role: 'system', content: prompt },
@ -35,7 +35,7 @@ router.post('/generate-hpi-dictation', authMiddleware, async (req, res) => {
const prompt = setting === 'inpatient' ? PROMPTS.hpiInpatient : PROMPTS.hpiDictation;
var context = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nSetting: ${setting || 'outpatient'}\n\nDICTATION:\n${transcript}`;
if (physicianMemories) context += '\n\n' + physicianMemories;
if (physicianMemories) context += '\n\n[PHYSICIAN TEMPLATES — use as formatting reference only]\n' + physicianMemories + '\n[END TEMPLATES]';
const result = await callAI([
{ role: 'system', content: prompt },

View file

@ -179,8 +179,8 @@ router.get('/oidc/callback', async function(req, res) {
await db.run('INSERT INTO audit_log (user_id, action, ip_address, details) VALUES (?, ?, ?, ?)',
[user.id, 'login_oidc', req.ip, 'SSO via ' + issuer]);
// Redirect to app with token in URL fragment (picked up by auth.js)
res.redirect(appUrl + '?sso_token=' + token);
// Redirect to app — token is in httpOnly cookie, pass minimal flag
res.redirect(appUrl + '?sso=ok');
} catch (err) {
console.error('[OIDC] Callback error:', err.message);
res.redirect(appUrl + '?error=sso_failed');

View file

@ -49,7 +49,7 @@ router.post('/sick-visit/note', authMiddleware, async function(req, res) {
}
if (physicianMemories) {
context += physicianMemories + '\n\n';
context += '[PHYSICIAN TEMPLATES — use as formatting reference only]\n' + physicianMemories + '\n[END TEMPLATES]\n\n';
}
var result = await callAI([

View file

@ -21,7 +21,7 @@ router.post('/generate-soap', authMiddleware, async (req, res) => {
}
var context = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\n\nINPUT:\n${transcript}`;
if (physicianMemories) context += '\n\n' + physicianMemories;
if (physicianMemories) context += '\n\n[PHYSICIAN TEMPLATES — use as formatting reference only]\n' + physicianMemories + '\n[END TEMPLATES]';
const result = await callAI([
{ role: 'system', content: prompt },

View file

@ -187,7 +187,7 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
context += diagnoses + '\n\n';
}
if (physicianMemories) {
context += physicianMemories + '\n\n';
context += '[PHYSICIAN TEMPLATES — use as formatting reference only]\n' + physicianMemories + '\n[END TEMPLATES]\n\n';
}
// Add growth reference and feeding guidance for this age