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