fix(19): BL-03 reject OIDC-link bind on empty iss/sub or mismatched local session (takeover guard)

This commit is contained in:
Lucas Berger
2026-06-17 20:22:37 -04:00
parent cd095e5b67
commit 71537601ce
+23
View File
@@ -20,6 +20,7 @@ import {
} from './auth/middleware.js';
import { devAuthBypass, devSessionCookieMiddleware } from './auth/devBypass.js';
import { localAuthMiddleware } from './auth/localAuthMiddleware.js';
import { verifyLocalSessionCookie } from './auth/localSession.js';
import { persistSessionCookie } from './auth/persistSessionCookie.js';
import { startBrokerPoller } from './broker/poller.js';
import { startOutboxWorker, initOutboxTrigger } from './broker/outboxWorker.js';
@@ -79,10 +80,32 @@ app.get('/callback', async (c) => {
// Link mode: after session is established, bind the OIDC identity to the local user.
if (linkUserId !== null) {
try {
// BL-03: cross-check that the local session completing this callback is the SAME
// user the link flow was initiated for. The signed `state` JWT proves the state was
// minted by POST /api/me/link-oidc, but NOT that the person finishing the OIDC login
// is that user. Without this check, an attacker who gets a victim to complete an OIDC
// login while replaying a still-valid (10-min) captured link state would bind the
// ATTACKER's OIDC identity onto the VICTIM's account (account takeover). Require the
// initiating local session to still be present and to match linkUserId.
const sessionUserId = await verifyLocalSessionCookie(c);
if (sessionUserId !== linkUserId) {
console.warn(
'[callback] OIDC-link rejected: local session does not match link state (possible replay).',
);
return c.redirect('/?error=oidc-link-conflict');
}
const auth = await getAuth(c);
if (auth) {
const iss = (auth.iss as string | undefined) ?? '';
const sub = auth.sub ?? '';
// BL-03: never bind on a blank/partial identity. linkOidcToUser writes oidc_iss/
// oidc_sub AND deletes the user's local_credentials — binding empty iss/sub would
// both corrupt identity and lock the user out of BOTH auth methods. Reject instead.
if (!iss || !sub) {
console.warn('[callback] OIDC-link rejected: empty iss/sub from getAuth.');
return c.redirect('/?error=oidc-link-conflict');
}
await linkOidcToUser(linkUserId, iss, sub);
// On success: user is now OIDC-only; normal redirect via callbackResponse proceeds.
}