webAuthn 可运行的 demo

写在前面,这是一个可以运行的 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>
相关推荐
赵大仁1 分钟前
深入解析前后端分离架构:原理、实践与最佳方案
前端·架构
学不动学不明白4 分钟前
PC端项目兼容手机端
前端
无名之逆5 分钟前
Hyperlane:轻量、高效、安全的 Rust Web 框架新选择
开发语言·前端·后端·安全·rust·github·ssl
wkj00111 分钟前
js给后端发送请求的方式有哪些
开发语言·前端·javascript
最新资讯动态18 分钟前
“RdbStore”上线开源鸿蒙社区 助力鸿蒙应用数据访问效率大幅提升
前端
magic 24519 分钟前
JavaScript运算符与流程控制详解
开发语言·前端·javascript
xulihang44 分钟前
在手机浏览器上扫描文档并打印
前端·javascript·图像识别
RR91 小时前
【Vue3 进阶👍】:如何批量导出子组件的属性和方法?从手动代理到Proxy的完整指南
前端·vue.js
加个鸡腿儿1 小时前
01实战案例:「新手向」如何将原始数据转换为前端可用选项?
前端·程序员
java1234_小锋1 小时前
一周学会Flask3 Python Web开发-SQLAlchemy更新数据操作-班级模块
前端·数据库·python·flask·flask3