Webauthn_server_npm_client_.../server/server.js
2025-11-26 15:44:25 +01:00

276 lines
7.7 KiB
JavaScript

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');
});