WebAuthn:用于JavaScript的服务器和客户端强认证库

区块链技术学习(微信号:Blockchain1024)翻译

原文链接:https://marmelab.com/blog/2019/06/24/web-authn-fido2-open-source-package.html


浏览器支持名为WebAuthn的新安全标准,以促进强认证。我们开源了一个库来促进它的实现。


强认证很复杂


让我们谈谈安全问题


密码很糟糕:当密码足够长、足够安全的时候,就很难记住了。我们需要为每个新服务生成一个新密码,并且禁止大约5.5亿个密码,因为它们是数据泄露的一部分。


有哪些替代身份验证方法?我们需要依靠拥有因素(用户拥有的东西,比如安全令牌)或内在因素(用户所拥有的东西,比如指纹),而不是知识因素(用户知道的东西)。或者,理想情况下,结合使用这些方法(即多因素身份验证,或MFA)。


遗憾的是,并非每个用户都拥有带指纹传感器的设备,而且软件令牌(如Google身份验证器应用程序)要求用户将一次性密码(OTP)从令牌复制到他们想要登录的服务。换句话说:强认证对于使用的终端来说是很麻烦的。


一个很好解决的方案是连接令牌,即插入计算机的设备,如USB密钥。这是一个占有因素,你可以在任何电脑上使用它。在连接到计算机时,你无需手动复制OTP。它使用公钥加密技术,只需单击即可验证质询。


这是一个连接的令牌,称为YubiKey,由Yubico公司销售:



但是,需要访问令牌的服务必须知道其协议。直到最近,只有胖客户端才能与连接的令牌进行通信。这排除了在浏览器中运行的服务。


WebAuthn / Fido2:一个W3C标准,W3C标准将强认证带入浏览器


输入FIDO2,这是浏览器中强认证的标准。引用维基百科:


FIDO2的核心是由W3C Web 身份验证 (WebAuthn)标准和FIDO 客户端到身份验证器协议(CTAP)组成。


WebAuthn标准是一组JavaScript API,用于在浏览器中与连接的令牌进行通信。在撰写本文时,大多数桌面浏览器都支持它,覆盖全球67%的用户:



它允许做的是使用连接的令牌注册并登录到Web应用程序。无需密码要:只需插上密钥,按下按钮,就可以了。


如果你曾经使用过YubiKey或类似的东西,你会同意这种体验比输入密码要好一个数量级。但是,为什么我们不在我们的web应用程序中使用WebAuthn呢?


客户端-服务器协议很难


还记得FIDO2项目中的客户端到认证器协议规范(CTAP)吗?它是一种协议,意味着它的实现取决于开发人员,问题就在这里。


已经有一些实现,你可以在GitHub和WebAuthn.io网站上找到。


WebAuthn.io官网:https://webauthn.io/



这些实现通常是由与FIDO2联盟成员开发的概念验证,这意味着这些库仅在少数情况下工作,并且它们的文档要么不存在,要么难以理解。


例如,下面是FIDO联盟自己编写的JavaScript webauthn-demo自述文件:


webauthn-demo地址:https://github.com/fido-alliance/webauthn-demo



它是JavaScript中唯一的一个。


介绍@webauthn包


现在,我们介绍@webauthn/client@webauthn/server这两个NPM包,它们将帮助JavaScript开发人员在实践中实现FIDO2。我们还在这些包中包含了一个示例客户端和服务器。


下面是如何在客户端实现注册按钮:


import { solveRegistrationChallenge, solveLoginChallenge } from '@webauthn/client';

const loginButton = document.getElementById('login');
const registerButton = document.getElementById('register');
const messageDiv = document.getElementById('message');

const displayMessage = message => {
    messageDiv.innerHTML = message;
}

registerButton.onclick = async () => {
    const challenge = await fetch('https://localhost:8000/request-register', {
        method'POST',
        headers: {
            'content-type''Application/Json'
        },
        bodyJSON.stringify({ id'uuid'email'test@test' })
    })
        .then(response => response.json());
    const credentials = await solveRegistrationChallenge(challenge);

    const { loggedIn } = await fetch(
        'https://localhost:8000/register'
        {
            method'POST',
            headers: {
                'content-type''Application/Json'
            },
            bodyJSON.stringify(credentials)
        }
    ).then(response => response.json());

    if (loggedIn) {
        displayMessage('registration successful');
        return;
    }
    displayMessage('registration failed');
};

loginButton.onclick = async () => {
    const challenge = await fetch('https://localhost:8000/login', {
        method'POST',
        headers: {
            'content-type''Application/Json'
        },
        bodyJSON.stringify({ email'test@test' })
    })
    .then(response => response.json());


    const credentials = await solveLoginChallenge(challenge);
    const { loggedIn } = await fetch(
        'https://localhost:8000/login-challenge'
        {
            method'POST',
            headers: {
                'content-type''Application/Json'
            },
            bodyJSON.stringify(credentials)
        }
    ).then(response => response.json());

    if (loggedIn) {
        displayMessage('You are logged in');
        return;
    }
    displayMessage('Invalid credential');
};


@webauthn/client公开的两个方法只是Navigator.credentials API的一个瘦包装器。实际上,浏览器管理加密挑战,这样开发人员就不会尝试自己完成(错误的方式)。


下面是服务器部分,使用Node.js和Express.js:


const {
    generateRegistrationChallenge,
    parseRegisterRequest,
} = require('@webauthn/server');

const app = express();
app.use(cors());
app.use(bodyParser());

app.post('/request-register', (req, res) => {
    const { id, email } = req.body;

    const challengeResponse = generateRegistrationChallenge({
        relyingParty: { name: 'ACME' },
        user: { id, name: email }
    });

    userRepository.create({
        id, 
        email,
        challenge: challengeResponse.challenge,
    })

    res.send(challengeResponse);
});

app.post('/register', (req, res) => {
    const { key, challenge } = parseRegisterRequest(req.body);

    const user = userRepository.findByChallenge(challenge);

    if (!user) {
        return res.sendStatus(400);
    }

    userRepository.addKeyToUser(user, key);

    return res.send({ loggedIn: true });
});

const config = {
    cert: fs.readFileSync(path.resolve(__dirname, '../tls/localhost.pem')),
    key: fs.readFileSync(path.resolve(__dirname, '../tls/localhost-key.pem'))
};

spdy.createServer(config, app).listen(8000, () => {
    console.log('Server is listening at https://localhost:8000. Ctrl^C to stop it.');
});


该代码可在GitHub的Apache2许可下获得:wallix / webauthn。

wallix / webauthn地址:https://github.com/wallix/webauthn


结论


安全性很难,新兴技术也很难,而且当你独自一人的时候,两者就更加困难。通过在FIDO2协议上分享我们为Wallix学习和构建的内容,我们希望你能够更轻松地在Web应用程序上实现强认证或双因素身份验证。



关于作者:

François Zaninotto

Marmelab创始人兼首席执行官,热衷于领导力、敏捷性、网络技术和开源。维护流行的开源库(Faker、admin-on-rest、gremlin .js),并定期在技术会议上发言。



●编号265,输入编号直达本文

●输入m获取文章目录

评论