webauthn_server_clients/server/server.js
2025-12-07 15:17:29 +01:00

303 lines
8.2 KiB
JavaScript

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