first commit

This commit is contained in:
Fabio 2025-11-26 15:44:25 +01:00
commit 004e606d84
7 changed files with 1860 additions and 0 deletions

68
README.md Normal file
View file

@ -0,0 +1,68 @@
# Webauthn server npm e client npx
## Server
questi sono i packaging da installare
```sh
├── @simplewebauthn/server@13.2.2
├── base64url@3.0.1
├── cookie-parser@1.4.7
├── cors@2.8.5
├── express-session@1.18.2
├── express@4.21.2
├── jsonwebtoken@9.0.2
└── uuid@13.0.0
```
creare la directory inizializzare con
```sh
npm init -y
npm install @simplewebauthn/server base64url cookie-parser cors express-session express jsonwebtoken uuid
```
creare il server.js vedi folder server e lanciare con
```sh
node server.js
```
## Client in npm
creare il file index.html vedi folder client
e lanciare con
```sh
npx serve .
```
la porta standard è la 3000
se si vuole utilizzare un'altra usare il parametro -l (x es 3100)
```sh
npx serve . -l 3100
```
## Setting di Nginx per far funzionare entrambi
tenendo in considerazione questi parametri
```
server:
porta 3400
ip 192.168.1.3
indirizzo auth.patachina.it
client:
porta 3000
ip 192.168.1.3
indirizzo my.patachina.casacam.net
```
utilizzare i file nel folder nginx
```
nginx_client.conf
nginx_server.conf
```

110
client/index.html Normal file
View file

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>WebAuthn Client</title>
</head>
<body>
<h1>Demo WebAuthn</h1>
<!-- Casella di input per lo username -->
<label for="username">Username:</label>
<input type="text" id="username" value="fabio" />
<button id="registerBtn">Registrati</button>
<button id="loginBtn">Login</button>
<script type="module">
import {
startRegistration,
startAuthentication
} from 'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser/+esm';
const API_BASE = 'https://auth.patachina.it';
function getUsername() {
return document.getElementById('username').value.trim();
}
async function registerUser() {
try {
const username = getUsername();
const resp = await fetch(`${API_BASE}/my/register-options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ username: 'fabio' }),
body: JSON.stringify({ username }),
credentials: 'include',
});
const options = await resp.json();
const attResp = await startRegistration(options);
console.log('attResp: ',attResp);
const payload = {
username: 'fabio',
attestationResponse: attResp
};
console.log(payload);
const verif = await fetch(`${API_BASE}/my/register-verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'include',
})
const verification = await verif.json();
console.log('verification: ', verification);
alert(`Registrazione: ${verification.success ? 'OK' : 'Fallita'}`);
} catch (err) {
console.error(err);
alert('Errore nella registrazione');
}
}
async function loginUser() {
try {
const username = getUsername();
const resp = await fetch(`${API_BASE}/my/login-options`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
/* body: JSON.stringify({
username: 'fabio',
}, // tutto loggetto di risposta
), */
body: JSON.stringify({ username }),
credentials: 'include',
});
const options = await resp.json();
if (!resp.ok) {
// Qui uso il campo message inviato dal server
throw new Error(options.message || 'Errore sconosciuto');
}
console.log('options: ',options);
const authResp = await startAuthentication(options);
console.log('authResp: ',authResp);
const verif = await fetch(`${API_BASE}/my/login-verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(
{username: 'fabio',
assertionResponse: authResp}, // tutto loggetto di risposta
),
credentials: 'include',
});
const verification = await verif.json();
console.log('verification: ', verification);
//alert(JSON.stringify(verification));
alert(`Login: ${verification.success ? 'OK' : 'Fallito'}`);
} catch (err) {
console.error(err);
alert(`Login fallito: ${err.message}`);
}
}
document.getElementById('registerBtn').addEventListener('click', registerUser);
document.getElementById('loginBtn').addEventListener('click', loginUser);
</script>
</body>
</html>

26
nginx/nginx_client.conf Normal file
View file

@ -0,0 +1,26 @@
server {
listen 443 ssl;
server_name my.patachina2.casacam.net;
# Certificati SSL (sostituisci con i tuoi)
ssl_certificate ssl/live/patachina2.casacam.net/fullchain.pem;
ssl_certificate_key ssl/live/patachina2.casacam.net/privkey.pem;
# Proxy verso il server statico (npx serve)
location / {
proxy_pass http://192.168.1.3:3000; # npx serve gira su questa porta
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Mantieni connessioni aperte
proxy_set_header Connection "";
}
# Opzionale: sicurezza base
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
}

26
nginx/nginx_server.conf Normal file
View file

@ -0,0 +1,26 @@
server {
listen 443 ssl;
server_name my.patachina2.casacam.net;
# Certificati SSL (sostituisci con i tuoi)
ssl_certificate ssl/live/patachina2.casacam.net/fullchain.pem;
ssl_certificate_key ssl/live/patachina2.casacam.net/privkey.pem;
# Proxy verso il server statico (npx serve)
location / {
proxy_pass http://192.168.1.3:3000; # npx serve gira su questa porta
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Mantieni connessioni aperte
proxy_set_header Connection "";
}
# Opzionale: sicurezza base
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
}

1336
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

18
server/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "authn-server",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@simplewebauthn/server": "^13.2.0",
"base64url": "^3.0.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-session": "^1.18.2",
"jsonwebtoken": "^9.0.2",
"uuid": "^13.0.0"
}
}

276
server/server.js Normal file
View file

@ -0,0 +1,276 @@
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');
});