Phase 19: Local Auth (No-OIDC Mode) #23
@@ -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.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user