Skip to content

Commit 0b5d864

Browse files
committed
update change user workflow
1 parent 493c5e9 commit 0b5d864

1 file changed

Lines changed: 61 additions & 34 deletions

File tree

src/idp/interactions.js

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { authenticate, findById, findByWebId, createAccount, updateLastLogin, setPasskeyPromptDismissed } from './accounts.js';
77
import { loginPage, consentPage, errorPage, registerPage, passkeyPromptPage } from './views.js';
8+
import { createAdapter } from './adapter.js';
89
import * as storage from '../storage/filesystem.js';
910
import { createPodStructure } from '../handlers/container.js';
1011
import { validateInvite } from './invites.js';
@@ -13,6 +14,42 @@ import { verifyNostrAuth } from '../auth/nostr.js';
1314
// Security: Maximum body size for IdP form submissions (1MB)
1415
const MAX_BODY_SIZE = 1024 * 1024;
1516

17+
async function finishInteractionOrError(request, reply, provider, result, options = { mergeWithLastSubmission: false }) {
18+
try {
19+
reply.hijack();
20+
await provider.interactionFinished(request.raw, reply.raw, result, options);
21+
} catch (err) {
22+
request.log.error({ err: err.message, uid: request.params?.uid }, 'interactionFinished failed');
23+
if (!reply.raw.writableEnded) {
24+
reply.raw.statusCode = 500;
25+
reply.raw.setHeader('Content-Type', 'text/html; charset=utf-8');
26+
reply.raw.end(errorPage('Error', 'Could not complete interaction. Please try again.'));
27+
}
28+
}
29+
}
30+
31+
async function clearInteractionSession(interaction) {
32+
const sessionCookie = interaction.session?.cookie;
33+
if (!sessionCookie) return;
34+
35+
const sessionAdapter = createAdapter('Session');
36+
const sessionData = await sessionAdapter.find(sessionCookie);
37+
if (!sessionData) return;
38+
39+
delete sessionData.accountId;
40+
delete sessionData.loginTs;
41+
delete sessionData.amr;
42+
delete sessionData.acr;
43+
delete sessionData.transient;
44+
sessionData.authorizations = {};
45+
46+
const expiresIn = sessionData._expiresAt
47+
? Math.max(1, Math.ceil((sessionData._expiresAt - Date.now()) / 1000))
48+
: undefined;
49+
50+
await sessionAdapter.upsert(sessionCookie, sessionData, expiresIn);
51+
}
52+
1653
/**
1754
* Handle GET /idp/interaction/:uid
1855
* Shows login or consent page based on interaction state
@@ -156,17 +193,15 @@ export async function handleLogin(request, reply, provider) {
156193
};
157194

158195
// Save the login result to the interaction.
159-
// The result is stored here; oidc-provider will read it when we redirect
160-
// back to the authorization endpoint via returnTo.
161196
const returnTo = interaction.returnTo;
162197
interaction.result = result;
163198
await interaction.save(interaction.exp - Math.floor(Date.now() / 1000));
164199

165-
// For browsers: redirect to the authorization endpoint so oidc-provider
166-
// can complete the flow. Avoids reply.hijack() + interactionFinished()
167-
// which could hang the response if the provider throws internally.
200+
// For browsers: let oidc-provider finalize the interaction.
201+
// This binds the authenticated account to the OIDC session.
168202
if (wantsBrowserRedirect) {
169-
return reply.redirect(returnTo);
203+
await finishInteractionOrError(request, reply, provider, result, { mergeWithLastSubmission: false });
204+
return;
170205
}
171206

172207
// For CTH and programmatic clients: return JSON with location
@@ -227,16 +262,8 @@ export async function handleConsent(request, reply, provider) {
227262
},
228263
};
229264

230-
// Mark reply as sent since interactionFinished will handle the response
231-
reply.hijack();
232-
233-
// Use interactionFinished which handles the redirect directly
234-
return provider.interactionFinished(
235-
request.raw,
236-
reply.raw,
237-
result,
238-
{ mergeWithLastSubmission: true }
239-
);
265+
await finishInteractionOrError(request, reply, provider, result, { mergeWithLastSubmission: true });
266+
return;
240267
} catch (err) {
241268
request.log.error(err, 'Consent error');
242269
return reply.code(500).type('text/html').send(errorPage('Consent failed', err.message));
@@ -257,14 +284,8 @@ export async function handleAbort(request, reply, provider) {
257284
};
258285

259286
// oidc-provider is configured with /idp routes, so redirectTo will have correct path
260-
const redirectTo = await provider.interactionResult(
261-
request.raw,
262-
reply.raw,
263-
result,
264-
{ mergeWithLastSubmission: false }
265-
);
266-
267-
return reply.redirect(redirectTo);
287+
await finishInteractionOrError(request, reply, provider, result, { mergeWithLastSubmission: false });
288+
return;
268289
} catch (err) {
269290
request.log.error(err, 'Abort error');
270291
return reply.code(500).type('text/html').send(errorPage('Error', err.message));
@@ -284,6 +305,8 @@ export async function handleRelogin(request, reply, provider) {
284305
return reply.code(404).type('text/html').send(errorPage('Interaction not found', 'This login session has expired. Please try again.'));
285306
}
286307

308+
await clearInteractionSession(interaction);
309+
287310
if (interaction.session && interaction.session.accountId) {
288311
delete interaction.session.accountId;
289312
}
@@ -506,8 +529,8 @@ export async function handlePasskeyComplete(request, reply, provider) {
506529

507530
request.log.info({ accountId: account.id, uid }, 'Passkey login completed');
508531

509-
reply.hijack();
510-
return provider.interactionFinished(request.raw, reply.raw, result, { mergeWithLastSubmission: false });
532+
await finishInteractionOrError(request, reply, provider, result, { mergeWithLastSubmission: false });
533+
return;
511534
} catch (err) {
512535
request.log.error(err, 'Passkey complete error');
513536
return reply.code(500).type('text/html').send(errorPage('Error', err.message));
@@ -533,19 +556,23 @@ export async function handlePasskeySkip(request, reply, provider) {
533556
}
534557

535558
// Get the pending login result
536-
const result = interaction.result;
537-
if (!result?.login?.accountId) {
559+
const pending = interaction.result;
560+
if (!pending?.login?.accountId) {
538561
return reply.code(400).type('text/html').send(errorPage('Invalid state', 'No pending login found.'));
539562
}
540563

541564
// Mark passkey prompt as dismissed so we don't nag again
542-
await setPasskeyPromptDismissed(result.login.accountId, true);
565+
await setPasskeyPromptDismissed(pending.login.accountId, true);
543566

544-
request.log.info({ accountId: result.login.accountId, uid }, 'Passkey prompt skipped');
567+
request.log.info({ accountId: pending.login.accountId, uid }, 'Passkey prompt skipped');
545568

546569
// Complete the OIDC interaction
547-
reply.hijack();
548-
return provider.interactionFinished(request.raw, reply.raw, result, { mergeWithLastSubmission: false });
570+
const result = {
571+
login: pending.login,
572+
};
573+
574+
await finishInteractionOrError(request, reply, provider, result, { mergeWithLastSubmission: false });
575+
return;
549576
} catch (err) {
550577
request.log.error(err, 'Passkey skip error');
551578
return reply.code(500).type('text/html').send(errorPage('Error', err.message));
@@ -661,8 +688,8 @@ export async function handleSchnorrComplete(request, reply, provider) {
661688

662689
request.log.info({ accountId: account.id, uid }, 'Schnorr login completed');
663690

664-
reply.hijack();
665-
return provider.interactionFinished(request.raw, reply.raw, interaction.result, { mergeWithLastSubmission: false });
691+
await finishInteractionOrError(request, reply, provider, interaction.result, { mergeWithLastSubmission: false });
692+
return;
666693
} catch (err) {
667694
request.log.error(err, 'Schnorr complete error');
668695
return reply.code(500).type('text/html').send(errorPage('Error', err.message));

0 commit comments

Comments
 (0)