From 376feecb280c28504788c9677c6cb3cc455f00b6 Mon Sep 17 00:00:00 2001 From: Pawel Zelawski Date: Sat, 23 May 2026 10:33:53 +0200 Subject: chore: upgrade digiid-ts to v3 and stabilize dev/build scripts --- src/server/main.ts | 316 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 179 insertions(+), 137 deletions(-) (limited to 'src/server/main.ts') diff --git a/src/server/main.ts b/src/server/main.ts index 96afb0e..1ab9004 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -28,157 +28,199 @@ console.log(`Attempting to listen on port: ${PORT}`); console.log(`Configured PUBLIC_URL: ${process.env.PUBLIC_URL}`); // Endpoint to initiate the DigiID authentication flow -app.get('/api/digiid/start', (req: Request, res: Response, next: NextFunction) => { - (async () => { - try { - const sessionId = randomBytes(16).toString('hex'); - const nonce = randomBytes(16).toString('hex'); - - const publicUrl = process.env.PUBLIC_URL; - if (!publicUrl) { - console.error('PUBLIC_URL environment variable is not set.'); - return res.status(500).json({ error: 'Server configuration error: PUBLIC_URL is missing.' }); - } - - let callbackUrl: string; +app.get( + '/api/digiid/start', + (req: Request, res: Response, next: NextFunction) => { + (async () => { try { - const baseUrl = new URL(publicUrl); - callbackUrl = new URL('/api/digiid/callback', baseUrl).toString(); + const sessionId = randomBytes(16).toString('hex'); + const nonce = randomBytes(16).toString('hex'); + + const publicUrl = process.env.PUBLIC_URL; + if (!publicUrl) { + console.error('PUBLIC_URL environment variable is not set.'); + return res.status(500).json({ + error: 'Server configuration error: PUBLIC_URL is missing.', + }); + } + + let callbackUrl: string; + try { + const baseUrl = new URL(publicUrl); + callbackUrl = new URL('/api/digiid/callback', baseUrl).toString(); + } catch (error) { + console.error('Invalid PUBLIC_URL format:', publicUrl, error); + return res.status(500).json({ + error: 'Server configuration error: Invalid PUBLIC_URL format.', + }); + } + + const unsecure = callbackUrl.startsWith('http://'); + // Use the actual function from digiid-ts + const digiIdUri = generateDigiIDUri({ callbackUrl, unsecure, nonce }); + console.log( + `Generated DigiID URI: ${digiIdUri} for session ${sessionId}` + ); + + sessionStore.set(sessionId, { nonce, status: 'pending' }); + nonceToSessionMap.set(nonce, sessionId); + console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`); + + const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri); + res.json({ sessionId, qrCodeDataUrl }); } catch (error) { - console.error('Invalid PUBLIC_URL format:', publicUrl, error); - return res.status(500).json({ error: 'Server configuration error: Invalid PUBLIC_URL format.' }); + console.error('Error in /api/digiid/start:', error); + // Ensure response is sent even on error + if (!res.headersSent) { + // Check if response hasn't already been sent + if (error instanceof Error) { + res + .status(400) + .json({ error: `Failed to generate URI: ${error.message}` }); + } else { + res + .status(500) + .json({ error: 'Internal server error during start' }); + } + } } + })().catch(next); + } +); - const unsecure = callbackUrl.startsWith('http://'); - // Use the actual function from digiid-ts - const digiIdUri = generateDigiIDUri({ callbackUrl, unsecure, nonce }); - console.log(`Generated DigiID URI: ${digiIdUri} for session ${sessionId}`); +// Callback endpoint for the DigiID mobile app +app.post( + '/api/digiid/callback', + (req: Request, res: Response, next: NextFunction) => { + (async () => { + const { address, uri, signature } = req.body; + + // Basic validation of received data + if (!address || !uri || !signature) { + console.warn('Callback missing required fields.', { + address, + uri, + signature, + }); + // Wallet doesn't expect a body on failure, just non-200. Status only for logging/debug. + return res.status(400).send('Missing required callback parameters.'); + } - sessionStore.set(sessionId, { nonce, status: 'pending' }); - nonceToSessionMap.set(nonce, sessionId); - console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`); + const callbackData = { address, uri, signature }; + console.log('Received callback:', callbackData); - const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri); - res.json({ sessionId, qrCodeDataUrl }); + // --- Nonce Extraction and Session Lookup --- + let receivedNonce: string | null = null; + try { + // DigiID URIs need scheme replaced for standard URL parsing + const parsableUri = uri.replace(/^digiid:/, 'http:'); + const parsedUri = new URL(parsableUri); + receivedNonce = parsedUri.searchParams.get('x'); + } catch (error) { + console.warn('Error parsing received URI:', uri, error); + return res.status(400).send('Invalid URI format.'); + } - } catch (error) { - console.error('Error in /api/digiid/start:', error); - // Ensure response is sent even on error - if (!res.headersSent) { // Check if response hasn't already been sent - if (error instanceof Error) { - res.status(400).json({ error: `Failed to generate URI: ${error.message}` }); - } else { - res.status(500).json({ error: 'Internal server error during start' }); - } + if (!receivedNonce) { + console.warn('Nonce (x parameter) not found in received URI:', uri); + return res.status(400).send('Nonce not found in URI.'); } - } - })().catch(next); -}); -// Callback endpoint for the DigiID mobile app -app.post('/api/digiid/callback', (req: Request, res: Response, next: NextFunction) => { - (async () => { - const { address, uri, signature } = req.body; - - // Basic validation of received data - if (!address || !uri || !signature) { - console.warn('Callback missing required fields.', { address, uri, signature }); - // Wallet doesn't expect a body on failure, just non-200. Status only for logging/debug. - return res.status(400).send('Missing required callback parameters.'); - } - - const callbackData = { address, uri, signature }; - console.log('Received callback:', callbackData); - - // --- Nonce Extraction and Session Lookup --- - let receivedNonce: string | null = null; - try { - // DigiID URIs need scheme replaced for standard URL parsing - const parsableUri = uri.replace(/^digiid:/, 'http:'); - const parsedUri = new URL(parsableUri); - receivedNonce = parsedUri.searchParams.get('x'); - } catch (error) { - console.warn('Error parsing received URI:', uri, error); - return res.status(400).send('Invalid URI format.'); - } - - if (!receivedNonce) { - console.warn('Nonce (x parameter) not found in received URI:', uri); - return res.status(400).send('Nonce not found in URI.'); - } - - const sessionId = nonceToSessionMap.get(receivedNonce); - if (!sessionId) { - console.warn('Session not found for received nonce:', receivedNonce); - // Nonce might be expired or invalid - return res.status(404).send('Session not found or expired for this nonce.'); - } - - // Retrieve the session *before* the try/finally block for verification - const session = sessionStore.get(sessionId); - if (!session) { - console.error(`Critical: Session data missing for ${sessionId} despite nonce match.`); - if (!res.headersSent) res.status(500).send('Internal server error: Session data missing.'); + const sessionId = nonceToSessionMap.get(receivedNonce); + if (!sessionId) { + console.warn('Session not found for received nonce:', receivedNonce); + // Nonce might be expired or invalid + return res + .status(404) + .send('Session not found or expired for this nonce.'); + } + + // Retrieve the session *before* the try/finally block for verification + const session = sessionStore.get(sessionId); + if (!session) { + console.error( + `Critical: Session data missing for ${sessionId} despite nonce match.` + ); + if (!res.headersSent) + res.status(500).send('Internal server error: Session data missing.'); return; // Explicitly return void - } - if (session.status !== 'pending') { + } + if (session.status !== 'pending') { console.warn('Session already processed:', sessionId, session.status); - if (!res.headersSent) res.status(200).send('Session already processed.'); + if (!res.headersSent) + res.status(200).send('Session already processed.'); + return; // Explicitly return void + } + + // --- Verification --- + let expectedCallbackUrl: string; + try { + const publicUrl = process.env.PUBLIC_URL; + if (!publicUrl) + throw new Error( + 'PUBLIC_URL environment variable is not configured on the server.' + ); + expectedCallbackUrl = new URL( + '/api/digiid/callback', + publicUrl + ).toString(); + } catch (error) { + console.error( + 'Server configuration error constructing expected callback URL:', + error + ); + session.status = 'failed'; + session.error = 'Server configuration error preventing verification.'; + sessionStore.set(sessionId, session); + // Respond 200 OK as per protocol + if (!res.headersSent) res.status(200).send(); return; // Explicitly return void - } - - // --- Verification --- - let expectedCallbackUrl: string; - try { - const publicUrl = process.env.PUBLIC_URL; - if (!publicUrl) throw new Error('PUBLIC_URL environment variable is not configured on the server.'); - expectedCallbackUrl = new URL('/api/digiid/callback', publicUrl).toString(); - } catch (error) { - console.error('Server configuration error constructing expected callback URL:', error); - session.status = 'failed'; - session.error = 'Server configuration error preventing verification.'; - sessionStore.set(sessionId, session); - // Respond 200 OK as per protocol - if (!res.headersSent) res.status(200).send(); - return; // Explicitly return void - } - - const verifyOptions = { expectedCallbackUrl, expectedNonce: session.nonce }; - - try { - console.log('Attempting to verify callback with:', { callbackData, verifyOptions }); - await verifyDigiIDCallback(callbackData, verifyOptions); - - // Success case (no throw from verifyDigiIDCallback) - console.log(`Verification successful for session ${sessionId}, address: ${address}`); - session.status = 'success'; - session.address = address; // Store the verified address - session.error = undefined; // Clear any previous error - nonceToSessionMap.delete(session.nonce); // Clean up nonce map only on success - - } catch (error) { - // Failure case (verifyDigiIDCallback threw an error) - console.warn(`Verification failed for session ${sessionId}:`, error); - session.status = 'failed'; - if (error instanceof Error) { - session.error = error.message; - } else { - session.error = 'An unknown verification error occurred.'; } - // Optionally cleanup nonce map on failure too, depending on policy - // nonceToSessionMap.delete(session.nonce); - } finally { - // Update store and respond 200 OK in all cases (after try/catch) - sessionStore.set(sessionId, session); - console.log(`Final session state for ${sessionId}:`, session); - // Ensure response is sent if not already done (e.g. in case of unexpected error before finally) - if (!res.headersSent) { + + const verifyOptions = { + expectedCallbackUrl, + expectedNonce: session.nonce, + }; + + try { + console.log('Attempting to verify callback with:', { + callbackData, + verifyOptions, + }); + await verifyDigiIDCallback(callbackData, verifyOptions); + + // Success case (no throw from verifyDigiIDCallback) + console.log( + `Verification successful for session ${sessionId}, address: ${address}` + ); + session.status = 'success'; + session.address = address; // Store the verified address + session.error = undefined; // Clear any previous error + nonceToSessionMap.delete(session.nonce); // Clean up nonce map only on success + } catch (error) { + // Failure case (verifyDigiIDCallback threw an error) + console.warn(`Verification failed for session ${sessionId}:`, error); + session.status = 'failed'; + if (error instanceof Error) { + session.error = error.message; + } else { + session.error = 'An unknown verification error occurred.'; + } + // Optionally cleanup nonce map on failure too, depending on policy + // nonceToSessionMap.delete(session.nonce); + } finally { + // Update store and respond 200 OK in all cases (after try/catch) + sessionStore.set(sessionId, session); + console.log(`Final session state for ${sessionId}:`, session); + // Ensure response is sent if not already done (e.g. in case of unexpected error before finally) + if (!res.headersSent) { res.status(200).send(); + } + // No explicit return needed here as it's the end of the function } - // No explicit return needed here as it's the end of the function - } - })().catch(next); -}); + })().catch(next); + } +); // Endpoint to check the status of an authentication session app.get('/api/digiid/status/:sessionId', (req: Request, res: Response) => { @@ -200,4 +242,4 @@ app.get('/', (_: Request, res: Response) => { // Start the server app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); -}); \ No newline at end of file +}); -- cgit v1.2.3