无密码身份验证:安全、简单、快速部署

Passwordless 是一个用于 Node.js 的身份验证中间件,它能提高用户安全性,同时部署快速且简单。

近几个月来,对所有对 Web 安全和隐私感兴趣的人来说,都是令人兴奋的:出现了很多精彩的文章讨论 和演讲,同时也有大量的事件 加强了人们的意识。

然而,大多数网站仍然使用着从 Web 早期时代就存在的相同身份验证机制:用户名和密码。

虽然用户名和密码有其作用,但我们应该更加谨慎地考虑它们是否是项目中正确的解决方案。我们知道,大多数人在访问所有网站时都使用相同的密码。对于没有专门安全专家的项目,我们真的应该让用户冒着网站被入侵也会损害其亚马逊账户的风险吗?此外,传统机制默认情况下至少有两个攻击途径:登录页面和密码恢复页面。尤其是后者,通常是仓促实现的,因此天生就更具风险。

我们最近看到了不少好主意,其中一个非常直接且低技术的解决方案特别吸引了我:一次性密码。它们实现起来很快,攻击面很小,既不需要 QR 码也不需要 JavaScript。每当用户想要登录或会话失效时,她都会通过电子邮件或短信收到一个带有令牌的短期一次性链接。如果您想尝试一下,欢迎在 passwordless.net 上测试演示。

不幸的是,根据您的技术栈,现成的解决方案并不多。 Passwordless 为 Node.js 改变了这一现状。

Node.js 和 Express 入门

使用 Passwordless 入门非常简单,您可以在两个小时内为小型项目部署一个完整的安全身份验证解决方案。

$ npm install passwordless --save

提供了基本的框架。您还需要安装现有的存储接口之一,例如MongoStore,它可以安全地存储令牌。

$ npm install passwordless-mongostore --save

为了将令牌传递给用户,电子邮件是最常见的选项(但短信也是可行的),您可以自由选择任何现有的电子邮件框架,例如

$ npm install emailjs --save

设置基础

让我们在您用来初始化 Express 的同一个文件中,引入上面提到的所有模块。

var passwordless = require('passwordless');
var MongoStore = require('passwordless-mongostore');
var email   = require("emailjs");

如果您选择了emailjs 来传递令牌,那么现在也是将它连接到您的电子邮件帐户(例如 Gmail 帐户)的好时机。

var smtpServer  = email.server.connect({
   user:    yourEmail,
   password: yourPwd,
   host:    yourSmtp,
   ssl:     true
});

最后一步是告诉 Passwordless 您在上面选择哪个存储接口,并进行初始化。

// Your MongoDB TokenStore
var pathToMongoDb = 'mongodb://localhost/passwordless-simple-mail';
passwordless.init(new MongoStore(pathToMongoDb));

传递令牌

passwordless.addDelivery(deliver) 添加了一个新的传递机制。 deliver 在需要发送令牌时被调用。默认情况下,您选择的机制应该以以下格式向用户提供链接。

http://www.example.com/token={TOKEN}&uid={UID}

deliver 将使用所有必要的细节被调用。因此,令牌的传递(在本例中使用 emailjs)可以像这样简单。

passwordless.addDelivery(
    function(tokenToSend, uidToSend, recipient, callback) {
        var host = 'localhost:3000';
        smtpServer.send({
            text:    'Hello!nAccess your account here: http://'
            + host + '?token=' + tokenToSend + '&uid='
            + encodeURIComponent(uidToSend),
            from:    yourEmail,
            to:      recipient,
            subject: 'Token for ' + host
        }, function(err, message) {
            if(err) {
                console.log(err);
            }
            callback(err);
        });
});

初始化 Express 中间件

app.use(passwordless.sessionSupport());
app.use(passwordless.acceptToken({ successRedirect: '/'}));

sessionSupport() 使登录持久化,因此用户在浏览您的网站时将保持登录状态。请确保您之前已准备好会话中间件(例如 express-session)。

acceptToken() 将拦截任何传入的令牌,对用户进行身份验证,并将他们重定向到正确的页面。虽然 successRedirect 选项不是严格需要的,但强烈建议使用它来避免通过您网站上的外发 HTTP 链接的引用标头泄露有效令牌。

路由和身份验证

以下内容假定您已经按照express 文档 中的说明设置了路由器 var router = express.Router();

您至少需要两个 URL 来

  • 显示一个页面,要求用户输入他们的电子邮件地址
  • 接受表单详细信息(通过 POST)
/* GET: login screen */
router.get('/login', function(req, res) {
   res.render('login');
});

/* POST: login details */ router.post('/sendtoken', function(req, res, next) { // TODO: Input validation }, // Turn the email address into a user ID passwordless.requestToken( function(user, delivery, callback) { // E.g. if you have a User model: User.findUser(email, function(error, user) { if(error) { callback(error.toString()); } else if(user) { // return the user ID to Passwordless callback(null, user.id); } else { // If the user couldn’t be found: Create it! // You can also implement a dedicated route // to e.g. capture more user details User.createUser(email, '', '', function(error, user) { if(error) { callback(error.toString()); } else { callback(null, user.id); } }) } }) }), function(req, res) { // Success! Tell your users that their token is on its way res.render('sent'); });

这里发生了什么?passwordless.requestToken(getUserId) 有两个任务:确保电子邮件地址存在以及将它转换为一个唯一的用户 ID,可以通过电子邮件发送,并可用于以后识别用户。通常情况下,您已经有一个模型负责存储您的用户详细信息,您可以像上面的示例中那样与它进行交互。

在某些情况下(想想一个由少数用户编辑的博客),您也可以完全跳过用户模型,直接将有效的电子邮件地址与其相应的 ID 硬编码在一起。

var users = [
    { id: 1, email: 'marc@example.com' },
    { id: 2, email: 'alice@example.com' }
];

/* POST: login details */
router.post('/sendtoken',
    passwordless.requestToken(
        function(user, delivery, callback) {
            for (var i = users.length - 1; i >= 0; i--) {
                if(users[i].email === user.toLowerCase()) {
                    return callback(null, users[i].id);
                }
            }
            callback(null, null);
        }),
        // Same as above…

HTML 页面

它只需要一个简单的 HTML 表单,用来捕获用户的电子邮件地址。默认情况下,Passwordless 会查找一个名为 user 的输入字段。

    
        

Login

Email:

保护您的页面

Passwordless 提供中间件,以确保只有经过身份验证的用户才能看到某些页面。

/* Protect a single page */
router.get('/restricted', passwordless.restricted(),
 function(req, res) {
  // render the secret page
});

/* Protect a path with all its children */
router.use('/admin', passwordless.restricted());

谁登录了?

默认情况下,Passwordless 通过请求对象提供用户 ID:req.user。要显示或重用 ID,将其拉取数据库中的其他详细信息,您可以执行以下操作。

router.get('/admin', passwordless.restricted(),
    function(req, res) {
        res.render('admin', { user: req.user });
});

或者,更一般地说,您可以添加另一个中间件,从您的模型中拉取整个用户记录,并将其提供给您网站上的任何路由。

app.use(function(req, res, next) {
    if(req.user) {
        User.findById(req.user, function(error, user) {
            res.locals.user = user;
            next();
        });
    } else {
        next();
    }
})

就这样!

这就是让您的用户安全轻松地进行身份验证所需的全部内容。有关更多详细信息,您应该查看深入探讨,其中解释了所有选项和示例,它将向您展示如何将所有上述内容集成到一个有效的解决方案中。

评估

如前所述,所有身份验证系统都有其权衡,您应该根据自己的需求选择合适的系统。基于令牌的通道与大多数其他解决方案(包括经典的用户名/密码方案)共享一个风险:如果用户的电子邮件帐户被盗取,或者您 SMTP 服务器和用户之间的通道被盗取,那么用户在您网站上的帐户也会被盗取。有两个默认选项可以帮助降低(但不能完全消除!)这种风险:短期令牌和使用后自动使令牌失效。

对于大多数网站来说,基于令牌的身份验证代表着安全性的提升:用户不必想新的密码(通常太简单),并且没有用户重复使用密码的风险。对于我们开发者来说,Passwordless 提供了一个只有一种(且简单!)身份验证路径的解决方案,更容易理解,因此更容易保护。此外,我们不需要接触任何用户密码。

另一个要点是可用性。我们应该考虑第一次使用您的网站和后续登录两种情况。对于第一次使用您的网站的用户,基于令牌的身份验证再简单不过了:他们仍然需要验证他们的电子邮件地址,就像他们使用传统登录机制一样,但在最佳情况下,不需要任何额外的信息。不需要发挥创造力来想出符合所有限制条件的密码,也不需要记住任何东西。如果用户再次登录,体验将取决于具体的用例。大多数网站都有比较长的会话超时时间,登录相对较少。或者,人们访问网站的频率实际上很低,以至于他们难以回忆起自己是否曾经拥有过帐户,如果是的话,密码是什么。在这些情况下,Passwordless 在可用性方面具有明显优势。此外,需要执行的步骤很少,而且这些步骤可以在整个过程中非常清楚地解释。用户经常访问的网站或已经习惯每周登录几次的网站(想想亚马逊)可能更适合使用传统的(甚至更好的:双重验证)方法,因为人们可能会知道他们的密码,并且可能会有更多机会说服用户重视良好密码的重要性。

虽然 Passwordless 被认为是稳定的,但我仍然希望您在 GitHub 上提出评论和贡献,或者在 Twitter 上向我提出问题:@thesumofall

关于 Florian Heinemann

Florian 是麻省理工学院的系统设计和管理研究员,专注于复杂的社会技术系统。在加入空中客车公司担任知识和创新管理经理之前,他曾在企业软件领域的几家初创公司工作。Florian 对网络、安全、解决复杂问题和冬季运动充满热情。

更多 Florian Heinemann 的文章…

关于 Robert Nyman [荣誉编辑]

Mozilla Hacks 的技术布道师和编辑。发表有关 HTML5、JavaScript 和开放 Web 的演讲和博客文章。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年以来一直在从事 Web 前端开发工作,在瑞典和纽约市都有工作经历。他经常在http://robertnyman.com 上发表博客文章,并喜欢旅行和结识新朋友。

更多 Robert Nyman [荣誉编辑] 的文章…


38 条评论

  1. Alexandru

    为用户编写一个令牌和一个小的验证码,然后您只需在页面之间重新检查令牌,这不是更简单吗?

    您可以通过 GET 从一个页面发送到另一个页面,您只需要检查用户的 IP 地址和令牌的生存时间。

    2014 年 10 月 15 日 下午 05:41

    1. Florian Heinemann

      由于令牌通过推荐人不断暴露,这将造成安全风险:如果您通过点击离开网站,目标网站将拥有您的登录信息。这就是为什么无密码在第一次登录后会自动使令牌失效,并将您重定向到带有登录信息的 GET 查询的干净页面。

      2014 年 10 月 15 日 下午 06:02

      1. Eric Canales

        此外,如果您检查用户的 IP 地址是否相同,那么您将失去任何在代理后面的用户,因为他们的 IP 地址可能会改变。安全并非易事。通常,使用经过验证的系统更安全。

        2014 年 10 月 16 日 下午 05:34

  2. Sly

    那么,如果令牌是通过电子邮件发送的,并且电子邮件被泄露,整个系统就会被泄露吗?

    2014 年 10 月 15 日 下午 06:12

    1. Florian Heinemann

      是的。但是,传统登录系统也是如此:由于这些系统的密码重置功能,只要您有权访问电子邮件,就可以轻松破解传统系统。这个想法是,您让用户只记住一个复杂的密码,而不是为每个网站记住很多类似的/简单的密码。

      干杯

      Florian

      2014 年 10 月 15 日 下午 06:15

      1. David

        此外,在电子邮件中使用双重身份验证(在发生中间人攻击的情况下,再加上 SSL/TLS SMTP)应该可以进一步减轻这种风险。

        2014 年 10 月 15 日 下午 07:08

      2. Harsh

        如果攻击者使用传统登录系统的密码重置功能,用户会知道帐户被泄露,因为旧的凭据将不再有效,但在这里,入侵电子邮件帐户的人可以拥有一个永久性的后门,而用户对此一无所知。

        2014 年 10 月 15 日 上午 09:15

        1. Florian Heinemann

          我认为这是一个非常公平的观点。说实话,这可能仍然会逃脱我的注意:最近有太多由网站诱发的密码重置事件,因为安全漏洞+也许我只是忘记密码了?尽管如此,这是一个值得深入研究的观点!

          2014 年 10 月 15 日 上午 09:54

        2. Micah Strube

          可以添加一项额外的安全措施来减轻这种风险,即在验证未知设备时,要求使用第二步身份验证(SMS)。

          2014 年 10 月 16 日 下午 05:12

  3. Sean

    我对这个想法很感兴趣,我认为它可能非常重要。有人能说一下用户更改电子邮件的场景吗?乍一看,似乎没有简单的方法可以更改与帐户关联的电子邮件,因为令牌与电子邮件地址相关联。

    我认为在这种情况下,网站可以非常简单地提供“将其他电子邮件链接到此帐户”机制,因为登录后,电子邮件就会与该帐户关联。我假设这将由网站来实施,而不是这个中间件?

    2014 年 10 月 15 日 下午 06:16

    1. Florian Heinemann

      感谢您的反馈 :-) 令牌只与登录期间输入您的电子邮件地址和点击链接之间的短暂时间段内相关的地址绑定。因此,您可以在您的端轻松地拥有一个更改用户详细信息的界面 - 无需 Passwordless 参与。在下次登录时,Passwordless 会在您的数据库中查找用户并验证新地址。唯一可能发生的事情是:如果用户删除了旧的电子邮件地址,就无法再访问他们的帐户,但我认为这种情况非常罕见,可以通过离线流程来解决。

      Florian

      2014 年 10 月 15 日 下午 06:26

      1. mf

        在免费帐户的情况下很少见,但在工作场所和 ISP 提供的帐户中很常见。也许应该始终要求一个备份送达地址(电子邮件、短信、用户拥有的任何其他存储和转发系统),以进一步减少帮助台的呼叫量。

        2014 年 10 月 15 日 下午 16:43

  4. Lou

    从安全角度来看,这难道不是把责任推卸了吗?评估点通过提到现在用户有义务确保他们的电子邮件帐户不被泄露,以及 SMTP 服务器之间的路径不被泄露,指出了这一点。

    在大多数情况下,用户对这两个因素几乎没有控制权,或者对此一无所知。他们可以选择自己的密码,当然,但他们无法控制密码的存储位置以及如何保护它。SMTP 的想法也是如此,除非消息内容经过某种加密过程,以防止纯文本解析中间人攻击,否则迟早有人会发现它。

    2014 年 10 月 15 日 下午 06:26

    1. Florian Heinemann

      与传统机制相比,它实际上提高了安全性,因为它减少了攻击向量:传统方法也有一个通过电子邮件处理的密码重置功能。此外,您还有文章中提到的所有其他安全优势。如果您想避免使用电子邮件,您可以始终使用短信或任何其他媒介。

      Florian

      2014 年 10 月 15 日 下午 06:44

      1. Lou

        http://en.wikipedia.org/wiki/Short_Message_Service#Vulnerabilities

        在我看来,这仍然相当于推卸责任。但是,至少实施了无密码的网站不应为其他泄露数据通信模型负责。

        2014 年 10 月 15 日 下午 07:26

  5. stefan

    将所有网络浏览行为引导到邮箱旅行是阴险的,也是危险的。除了您为谷歌赚钱的事实(想想所有通过免费 Gmail 收件箱中的广告获得的广告收入增加),您还给除最大电子邮件提供商之外的所有电子邮件提供商带来了压力。这不是一个好的选择,与其他双重身份验证方法相比,并没有更好。与个性化集中的随机问题/照片相结合的验证码挑战会更好。怎么样,每次我想要一个安全的网站时,我就把我的 Instagram 信息提供给他们。他们会随机选择一张照片,然后问我拍摄照片的日期。

    2014 年 10 月 15 日 下午 06:38

    1. John

      作为一张随机 Instagram 照片的日期......你说去邮箱旅行是阴险的。:)

      2014 年 10 月 15 日 下午 06:58

  6. Sam

    我认为这是一个好主意。我第一次意识到,我只要拥有注册时使用的电子邮件,就可以恢复密码并拥有一个网站,我就想......为什么我需要密码?

    我认为我实施了一些我认为非常相似的东西。我要求用户输入电子邮件,对它进行哈希处理,并将其与唯一的 ID 存储在一个表中。然后我给用户发送了一封包含唯一 ID 的电子邮件。然后用户点击链接,并通过表记录进行验证。它很酷的地方在于,用户可以在一个浏览器中输入电子邮件,并在另一个浏览器中验证它,而且它都奏效了。

    一个区别是,我随后将哈希后的电子邮件存储在本地存储中,并且没有要求用户再次登录(除非他们擦除了本地存储,或者从其他浏览器登录)。

    我从来都不擅长安全、cookie、令牌之类的东西,所以我认为这是一个很差劲的想法,我只是偷懒了。也许不是!

    2014 年 10 月 15 日 上午 08:17

  7. dc0de

    我认为这是愚蠢的。

    现在您只是将身份验证传递给电子邮件系统。电子邮件系统很容易被破坏,依靠它们进行身份验证似乎目光短浅。

    也许这可以通过使用现有的已知双重身份验证系统来更好地实现,例如 Authy、Google Authenticator 或其他几个系统。

    2014 年 10 月 15 日 上午 09:09

    1. Florian Heinemann

      嘿,我认为已经有一些评论赞成/反对这个论点:它比常规机制更安全,因为它减少了攻击向量。更安全的解决方案确实存在,但也对用户来说更复杂。这不是为了保护您的 PayPal 帐户,而是为了保护风险较低的网站。作为一个额外的好处,它将提高人们 PayPal 帐户的安全性,因为大多数人会在所有网站上重复使用相同的密码。

      2014 年 10 月 15 日 上午 09:52

      1. Bj aka Bjantiques

        在当今时代,任何人都应该避免在两个或多个地方使用相同的密码,更不用说每个网站都使用相同的密码了。

        有了 RoboForm 之类的安全软件,很容易为每个网站创建极其安全的密码。

        每次都必须检查我的邮件的想法不利于高效工作,等待短信也不行。
        我也认为国际短信服务的成本是一个阻碍因素。

        Bj aka Bjantiques。

        2014 年 11 月 1 日 下午 13:49

  8. Aaron

    作为用 Node.js 构建的网站的所有者,我很想知道你能想出什么,但是,我感到失望。我没有看到任何真正的原因说明为什么它更容易。这类似于每次您想登录时都要进行密码重置。解释一下为什么要求某人每次想登录网站时都登录他们的电子邮件是“更人性化”的?发送短信也不容易。因为我必须在浏览器 URL 栏中输入令牌,或者最终使用我的手机浏览网站。我认为这没有好处......

    2014 年 10 月 15 日 上午 10:16

    1. Florian Heinemann

      嘿!这个解决方案旨在找到可用性和安全性的平衡。传统解决方案可能更容易(至少如果您知道密码...),但存在巨大的安全问题(请参阅最近的新闻)。此解决方案提高了安全性,但对于某些用例来说可能更复杂。不过,我相信有许多用例,在这个用例中,它实际上更容易使用(想想那些不太常访问的网站,或者人们不太愿意创建帐户,但没有问题只是提供他们的电子邮件地址,例如,然后他们就可以自定义网站/存储收藏夹/...)。

      2014 年 10 月 15 日 上午 10:24

  9. Sean

    @Florian:你上条评论里提到的场景概念引起了我的注意。有没有一个公开的文档,列出了所有登录场景?可能是我不知道它的存在,但我感觉,如果社区有一些指导方针,我们或许能更好地确定哪些解决方案在不同场景下最有效。概念指导在这里可能和工具集一样重要,我在这里看到的评论让我觉得,整理出一个类似需求矩阵的东西,对所有参与者来说都非常有益,并能促进围绕该主题的更深入讨论。并不是说我指望你去做这件事——我只是想知道这是否会有用。

    2014年10月15日 下午1:44

    1. Florian Heinemann

      我认为这是一个好主意!如果人们不再纠结于哪个解决方案绝对最好,而是思考如何为不同的网站提供最佳的安全/接受度/可用性/...平衡,这将极大地丰富讨论。

      我还没见过类似的东西,但我很乐意贡献。如果你想合作,请在 @thesumofall 联系我。

      干杯

      Florian

      2014年10月15日 下午1:56

    2. Florian Heinemann

      这可能是一个参考:https://twitter.com/fugueish/status/522606311423234049

      2014年10月15日 下午9:45

  10. Evan Owen

    我们在 Cotap (https://cotap.com) 使用并推广无密码身份验证已经超过一年了。用户的反馈非常积极,我们很高兴不再需要承担存储密码的风险。很高兴看到其他人也推广这种方式。

    2014年10月15日 下午8:38

    1. Florian Heinemann

      我认为这是一个无密码登录非常合理的应用场景!

      2014年10月15日 下午8:43

  11. Christopher

    致文章作者:我在前几段中发现了一些语法错误。这篇文章发表在 mozilla.com 上。请认真对待。

    2014年10月15日 下午10:20

    1. Robert Nyman [编辑]

      你可以选择建设性地提出你认为需要改进的地方,或者你可以抱怨。我们更希望你采取尊重的态度,并欢迎你提出任何好的建议。许多 Hacks 的作者并非英语母语人士,我宁愿让他们分享他们的知识、想法和经验,即使存在语法错误,也不愿让他们完全不分享。

      2014年10月16日 上午0:09

  12. Chris Peterson

    非常棒的想法!

    与其使用随机数,令牌可以是包含真实 OTP 的 Base62 编码(A-Za-z0-9)HMAC 数据块。这将使 URL 更美观,服务器前端可以通过检查拒绝伪造的登录令牌,阻止恶意令牌甚至接近你的数据库后端。

    此外,我认为登录 URL 不需要 UID。会话数据库可以将令牌映射到 UID。

    2014年10月15日 下午10:46

    1. Florian Heinemann

      很棒的建议!如果你愿意,也可以随时在 GitHub 上打开问题(这样我更容易跟进 ;-)

      关于 UID:一般来说,是的。无密码设计考虑了潜在的无会话使用场景。我认为这并非典型用例,因此可能值得将传递 UID 设置为可选!

      2014年10月16日 上午5:35

  13. Noitidart

    “每当用户想要登录或会话失效时,她会通过电子邮件或短信收到一个短期一次性链接,其中包含一个令牌。”

    哈哈

    无论如何,这很棒!感谢分享。

    2014年10月20日 上午0:41

  14. Caleb

    @ Florian 等等

    这样的系统不会通过要求用户通过电子邮件中的链接进行身份验证来强化“不良”的个人安全行为吗?

    2014年10月20日 上午11:21

    1. Florian Heinemann

      嗨,

      如果你担心潜在的网络钓鱼攻击,你可以轻松地通过电子邮件/短信发送令牌。然后用户需要手动输入令牌。这是一种可行的方案,但并非最理想的选择。到目前为止,即使是最大的公司(例如 Google)也选择使用链接选项,但我理解你的观点。

      Florian

      2014年10月20日 下午3:40

      1. Caleb

        说得有道理。

        我应该承认自己是一个开发新手。我目前正在学习编写安全的网络应用程序。希望你们不介意我问一些无知的问题。

        2014年10月20日 下午3:44

        1. Florian Heinemann

          我认为这是一个很好的问题,正如我们所见,即使是最大的公司也无法完全保护他们的系统。

          2014年10月20日 下午3:46

  15. Nathan

    BrowserID/Persona 发生了什么?它似乎仍在开发中,但我最近没有看到任何关于它的新闻。现在一切都变成了 Firefox 账户,这倒退了一步。

    2014年11月7日 上午9:31

本文评论已关闭。