first commit
This commit is contained in:
commit
004e606d84
7 changed files with 1860 additions and 0 deletions
68
README.md
Normal file
68
README.md
Normal 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
110
client/index.html
Normal 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 l’oggetto 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 l’oggetto 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
26
nginx/nginx_client.conf
Normal 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
26
nginx/nginx_server.conf
Normal 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
1336
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
server/package.json
Normal file
18
server/package.json
Normal 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
276
server/server.js
Normal 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');
|
||||
});
|
||||
Loading…
Reference in a new issue