const express = require('express'); const cors = require('cors'); const { v4: uuidv4 } = require('uuid'); const { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse, } = require('@simplewebauthn/server'); // CommonJS const { isoBase64URL } = require('@simplewebauthn/server/helpers'); const session = require('express-session'); const app = express(); app.set('trust proxy', 1); // dietro NGINX/HTTPS app.use(express.json()); app.use(session({ name: 'sid', secret: process.env.SESSION_SECRET || 'sono1chiave#moltolunga99esicura', resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: true, // true in produzione con HTTPS sameSite: 'none', // 'none' per cross-site; richiede secure:true maxAge: 24 * 60 * 60 * 1000 } })); app.use(cors({ origin: [ 'https://app.patachina.it', 'https://my.patachina2.casacam.net', 'http://localhost:5000' ], credentials: true })); const rpID = 'my.patachina2.casacam.net'; // Android emulator localhost const origin = `https://${rpID}`; const userDB = {}; // In-memory user store const appDB = {}; const user1DB = {}; const challregDB = {}; const challlogDB = {}; const uuDB = {}; const uidDB = {}; function isBase64Url(str) { return typeof str === 'string' && /^[A-Za-z0-9\-_]+$/.test(str); } const appMy = { id: 'my', appname: 'my', rpID: 'my.patachina2.casacam.net', rpName: 'my Local Authn service', origin: `https://my.patachina2.casacam.net`, } appDB['my'] = appMy; app.post('/:appId/register-options', async (req, res) => { try { console.log('start register options'); const appId = req.params.appId; const { username } = req.body; if (!appId || !username) { throw new Error('Missing appId or username'); } // Genera un nuovo UUID v4 const userIdString = uuidv4(); const userId = Buffer.from(userIdString, 'utf8'); const userId64 = isoBase64URL.fromBuffer(userId); const key = `${username}:${appId}`; uuDB[key] = { id: key, userId: userId64, date: new Date(), }; uidDB[userId64] = { id: userId64, app: appId, devices: [], }; const options = await generateRegistrationOptions({ rpName: appDB[appId]?.rpName || 'Default RP', rpID: appDB[appId]?.rpID || 'localhost', userID: userId, userName: username, // authenticatorSelection: { userVerification: 'preferred' } }); uidDB[userId64].challengereg = options.challenge; //console.log('options reg: ', options); //console.log('uidDB[userId64]: ', uidDB[userId64]); req.session.userID = userId64; //console.log('session reg opt', req.session); console.log('end register options user:', username); res.json(options); } catch (err) { console.error('❌ Error in register-options:', err); // Risposta di errore al client res.status(500).json({ error: 'Registration options failed', message: err.message, }); } }); app.post('/:appId/register-verify', async (req, res) => { try { const { username, attestationResponse } = req.body; console.log('start session reg ver'); const appId = req.params.appId; const userId64 = req.session.userID; if (!userId64 || !uidDB[userId64]) { throw new Error('User session or uidDB entry not found'); } const uid = uidDB[userId64]; //console.log('uid.challengereg: ', uid.challengereg); const verification = await verifyRegistrationResponse({ response: attestationResponse, expectedChallenge: uid.challengereg, expectedOrigin: appDB[appId]?.origin, expectedRPID: appDB[appId]?.rpID, // requireUserVerification: false, }); //console.log('verification: ', verification); delete uidDB[userId64].challengereg; const success = verification.verified; if (success) { uidDB[userId64].devices.push(verification.registrationInfo); //console.log('uidDB[userId64] dopo ver reg: ', uidDB[userId64]); console.log('success true'); } else { console.log('success false'); } // Gestione distruzione sessione con error handling req.session.destroy(err => { if (err) { console.error('❌ Errore cancellazione sessione:', err); return res.status(500).json({ success }); } res.clearCookie('sid').json({ success }); }); } catch (err) { console.error('❌ Error in register-verify:', err); res.status(500).json({ success: false, error: 'Registration verification failed', message: err.message, }); } }); app.post('/:appId/login-options', async (req, res) => { try { console.log('start session login opt'); const { username } = req.body; const appId = req.params.appId; if (!username || !appId) { throw new Error('Missing username or appId'); } const key = `${username}:${appId}`; if (!uuDB[key]) { console.log(`Utente ${username} non registrato`); return res.status(401).json({ success: false, message: `Utente ${username} non registrato`, }); } const userId64 = uuDB[key].userId; const user = uidDB[userId64]; //console.log('xxx devices:', user); if (!user || !user.devices || user.devices.length === 0) { return res.status(404).json({ error: 'User not found or no devices registered' }); } const all = { allowCredentials: user.devices.map(dev => ({ id: dev.credential.id, // base64url string type: 'public-key', transports: dev.transports || ['internal', 'hybrid'], })), userVerification: 'discouraged', // oppure 'preferred' / 'required' rpID: appDB[appId]?.rpID || 'localhost', timeout: 60000, }; const options = await generateAuthenticationOptions(all); uidDB[userId64].challengelog = options.challenge; //console.log('uidDB[userId64] ', uidDB[userId64]); console.log('end session logion options username:',username); req.session.userID = userId64; res.json(options); } catch (err) { console.error('❌ Error in login-options:', err); res.status(500).json({ success: false, error: 'Login options failed', message: err.message, }); } }); app.post('/:appId/login-verify', async (req, res) => { try { console.log('start session login verify'); const { username, assertionResponse } = req.body; const appId = req.params.appId; const userId64 = req.session.userID; if (!userId64 || !uidDB[userId64]) { throw new Error('User session or uidDB entry not found'); } const user = uidDB[userId64]; if (!user || !user.devices || user.devices.length === 0) { throw new Error('No devices registered for this user'); } let verified = false; let verification; for (const device of user.devices) { const verlog = { response: assertionResponse, expectedChallenge: user.challengelog, expectedOrigin: appDB[appId]?.origin, expectedRPID: appDB[appId]?.rpID, credential: device.credential, }; verification = await verifyAuthenticationResponse(verlog); if (verification.verified) { verified = true; // aggiorna il counter del device usato device.counter = verification.authenticationInfo.newCounter; break; } } delete uidDB[userId64].challengelog; // Risposta finale con gestione sessione req.session.destroy(err => { if (err) { console.error('❌ Errore cancellazione sessione:', err); return res.status(500).json({ success: verified }); } if (verified) { //console.log('uidDB[userId64] dopo ver log: ', uidDB[userId64]); console.log('success true'); return res.clearCookie('sid').json({ success: true }); } else { console.log('success false'); return res.clearCookie('sid').status(401).json({ success: false }); } }); } catch (err) { console.error('❌ Error in login-verify:', err); res.status(500).json({ success: false, error: 'Login verification failed', message: err.message, }); } }); app.listen(3400, '192.168.1.3', () => { console.log('WebAuthn server running on http://192.168.1.3:3400'); });