const express = require('express'); const cors = require('cors'); const base64url = require('base64url'); 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 cookieParser = require('cookie-parser'); 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 } })); /* const allowedOrigins = [ 'https://app.patachina.it', 'https://my.patachina2.casacam.net', 'http://localhost:5000' ]; app.use(cors({ origin: function (origin, callback) { if (!origin) return callback(null, true); // permette richieste server-to-server senza Origin if (allowedOrigins.includes(origin)) { return callback(null, origin); // restituisce SOLO l'origin valido } else { return callback(new Error('Not allowed by CORS')); } }, credentials: true })); */ 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) => { console.log('start register options'); const appId = req.params.appId; const { username } = req.body; // Genera un nuovo UUID v4 const userIdString = uuidv4(); // Se ti serve in formato Buffer per WebAuthn: 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 chall = { id: userId64, }; const options = await generateRegistrationOptions({ rpName: appDB[appId].rpName, rpID: appDB[appId].rpID, userID: userId, userName: username, /* authenticatorSelection: { userVerification: 'discouraged' // oppure 'preferred' se vuoi tentare o "required" se vuoi che chieda impronta digitale } */ }); 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); res.json(options); }); app.post('/:appId/register-verify', async (req, res) => { const { username, attestationResponse } = req.body; console.log('session reg ver: ',req.session); const appId = req.params.appId; const userId64 = req.session.userID; 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, // <-- disattiva requisito UV }); console.log('verification: ',verification); delete uidDB[userId64].challengereg; if (verification.verified) { uidDB[userId64].devices.push(verification.registrationInfo); console.log('uidDB[userId64] dopo ver reg: ', uidDB[userId64]); console.log('success true'); req.session.destroy(err => { if (err) { console.log("errore cancellazione sessione"); res.status(400).json({ success: true }); } else { res.clearCookie('sid').json({ success: true }); } }); } else { req.session.destroy(err => { if (err) { console.log("errore cancellazione sessione"); res.status(400).json({ success: false }); } else { res.clearCookie('sid').json({ success: false }); } }); } }); app.post('/:appId/login-options', async (req, res) => { // console.log('req.body', req.body); console.log('session login opt: ',req.session); const { username } = req.body; const appId = req.params.appId; const key = `${username}:${appId}`; if (uuDB[key]) { const userId64 = uuDB[key].userId; const user = uidDB[userId64]; console.log('xxx devices:',user); // console.log('user', user.devices[0].credential.id); if (!user || user.devices.length === 0) return res.status(404).json({ error: 'User not found' }); const all = { allowCredentials: user.devices.map(dev => ({ id: dev.credential.id, // <-- STRINGA base64url, non Buffer type: 'public-key', transports: dev.transports || ['internal', 'hybrid'], })), userVerification: 'discouraged', // o 'preferred' / 'required' rpID: appDB[appId].rpID, timeout: 60000, }; //console.log('all: ',all); const options = await generateAuthenticationOptions(all); //console.log('login options ',options); uidDB[userId64].challengelog = options.challenge; console.log('uidDB[userId64] ',uidDB[userId64]); req.session.userID = userId64; res.json(options); } else { console.log(`Utente ${username} non registrato`); res.status(401).json({ success: false, message: `Utente ${username} non registrato`}); }; }); app.post('/:appId/login-verify', async (req, res) => { const { username, assertionResponse } = req.body; const userId64 = req.session.userID; const user = uidDB[userId64]; const appId = req.params.appId; let verified = false; let verification; let verlog; for (const device of user.devices) { verlog = { response: assertionResponse, expectedChallenge: user.challengelog, //user.challenge, expectedOrigin: appDB[appId].origin, expectedRPID: appDB[appId].rpID, credential: device.credential, //credential: user.devices.map(d => d.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; if (verified) { console.log('uidDB[userId64] dopo ver log: ', uidDB[userId64]); console.log('success true'); req.session.destroy(err => { if (err) { console.log("errore cancellazione sessione"); res.status(500).json({ success: true }); } else { res.clearCookie('sid').json({ success: true }); } }); } else { req.session.destroy(err => { if (err) { console.log("errore cancellazione sessione"); res.status(401).json({ success: false }); } else { res.clearCookie('sid').status(401).json({ success: false }); } }); }; }); //app.listen(3400, () => console.log('WebAuthn server running on http://localhost:3400')); app.listen(3400, '192.168.1.3', () => { console.log('WebAuthn server running on http://192.168.1.3:3400'); });