写在前面,这是一个可以运行的 mac 指纹登录的 webauthn demo。使用的是 simplewebauthn 这个库实现的
package.json
json
{
"name": "webauthn",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^13.1.0",
"base64url": "^3.0.1",
"express": "^4.21.2"
}
}
server.js
js
const express = require('express');
const path = require('path');
const {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} = require('@simplewebauthn/server');
const { isoBase64URL, isoUint8Array, base64URLStringToBuffer } = require('@simplewebauthn/browser');
const base64URLToBuffer = (base64URL) => {
const base64 = base64URL.replace(/-/g, '+').replace(/_/g, '/');
return Buffer.from(base64, 'base64');
};
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// 模拟数据库
const users = new Map();
const rpName = 'WebAuthn Demo';
const rpID = 'localhost';
const origin = `http://${rpID}:3000`;
app.post('/register', async (req, res) => {
const username = req.body.username;
console.log(username, 'username')
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: Buffer.from(username), // buffer
userName: username,
attestationType: 'none',
authenticatorSelection: {
userVerification: 'required',
requireResidentKey: false,
},
});
console.log(options, 'options')
users.set(username, { currentChallenge: options.challenge });
res.json(options);
});
app.post('/register-verify', async (req, res) => {
const username = req.body.username;
const user = users.get(username);
if (!user) {
return res.status(400).send('User not found');
}
const expectedChallenge = user.currentChallenge;
try {
const verification = await verifyRegistrationResponse({
response: req.body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
});
if (verification.verified) {
user.registered = true;
user.currentChallenge = null;
// user.authenticator = verification.registrationInfo;
// console.log(verification.registrationInfo, 'register--register')
user.authenticator = {
credentialID: verification.registrationInfo.credential.id,
credentialPublicKey: verification.registrationInfo.credential.publicKey,
credentialDeviceType: verification.registrationInfo.credentialDeviceType,
credentialBackedUp: verification.registrationInfo.credentialBackedUp,
// 计数器防止克隆攻击
counter: verification.registrationInfo.credential.counter || 0,
};
console.log(user, 'register-user')
res.json({ success: true });
} else {
res.status(400).json({ success: false, message: 'Registration failed' });
}
} catch (error) {
console.error(error);
res.status(400).json({ success: false, message: error.message });
}
});
app.post('/login', async (req, res) => {
const username = req.body.username;
// 数据
const user = users.get(username);
if (!user || !user.registered) {
return res.status(400).send('User not registered');
}
const options = await generateAuthenticationOptions({
rpID,
userVerification: 'required',
allowCredentials: [{
id: user.authenticator.credentialID, // id
type: 'public-key',
}],
});
user.currentChallenge = options.challenge;
console.log(options, 'login-options')
res.json(options);
});
app.post('/login-verify', async (req, res) => {
const username = req.body.username;
const user = users.get(username);
if (!user) {
return res.status(400).send('User not found');
}
const expectedChallenge = user.currentChallenge;
try {
const verification = await verifyAuthenticationResponse({
response: req.body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
credential: {
id: user.authenticator.credentialID,
publicKey: user.authenticator.credentialPublicKey,
...user.authenticator,
},
});
console.log(verification, 'verification')
if (verification.verified) {
user.currentChallenge = null;
console.log(verification.authenticationInfo, 'authenticationInfo--authenticationInfo')
user.authenticator.counter = verification.authenticationInfo.newCounter;
res.json({ success: true });
} else {
res.status(400).json({ success: false, message: 'Authentication failed' });
}
} catch (error) {
console.error(error);
res.status(400).json({ success: false, message: error.message });
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
public/index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebAuthn Demo</title>
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
</head>
<body>
<h1>WebAuthn Demo</h1>
<input type="text" id="username" placeholder="Username">
<button onclick="register()">Register</button>
<button onclick="login()">Login</button>
<script>
async function register() {
const username = document.getElementById('username').value;
// Get registration options from server
const optionsRes = await fetch('/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsRes.json();
// Create credentials
const credential = await SimpleWebAuthnBrowser.startRegistration(options);
console.log(credential, 'credential--credential')
// Send credential to server for verification
const verificationRes = await fetch('/register-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, ...credential })
});
const verificationResult = await verificationRes.json();
if (verificationResult.success) {
alert('Registration successful!');
} else {
alert('Registration failed: ' + verificationResult.message);
}
}
async function login() {
const username = document.getElementById('username').value;
// Get authentication options from server
const optionsRes = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsRes.json();
// Perform authentication
const credential = await SimpleWebAuthnBrowser.startAuthentication(options);
// Send credential to server for verification
const verificationRes = await fetch('/login-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, ...credential })
});
const verificationResult = await verificationRes.json();
if (verificationResult.success) {
alert('Login successful!');
} else {
alert('Login failed: ' + verificationResult.message);
}
}
</script>
</body>
</html>