關(guān)于 Node.js 的認(rèn)證方面的教程(很可能)是有誤的
我搜索了大量關(guān)于 Node.js/Express.js 認(rèn)證的教程。所有這些都是不完整的,甚至以某種方式造成安全錯(cuò)誤,可能會(huì)傷害新用戶(hù)。當(dāng)其他教程不再幫助你時(shí),你或許可以看看這篇文章,這篇文章探討了如何避免一些常見(jiàn)的身份驗(yàn)證陷阱。同時(shí)我也一直在 Node/Express 中尋找強(qiáng)大的、一體化的解決方案,來(lái)與 Rails 的 devise 競(jìng)爭(zhēng)。
更新 (8.7): 在他們的教程中,RisingStack 已經(jīng)聲明,不要再以明文存儲(chǔ)密碼,在示例代碼和教程中選擇使用了 bcrypt。
更新 (8.8): 編輯標(biāo)題 關(guān)于 Node.js 的認(rèn)證方面的教程(很可能)是有誤的,這篇文章已經(jīng)對(duì)這些教程中的一些錯(cuò)誤點(diǎn)進(jìn)行了改正。
在業(yè)余時(shí)間,我一直在挖掘各種 Node.js 教程,似乎每個(gè) Node.js 開(kāi)發(fā)人員都有一個(gè)博客用來(lái)發(fā)布自己的教程,講述如何以正確的方式做事,或者更準(zhǔn)確地說(shuō),他們做事的方式。數(shù)以千計(jì)的前端開(kāi)發(fā)人員被投入到服務(wù)器端的 JS 漩渦中,試圖通過(guò)拷貝式的操作或無(wú)償使用的 npm install 將這些教程中的可操作的知識(shí)拼湊在一起,從而在外包經(jīng)理或廣告代理商給出的期限內(nèi)完成開(kāi)發(fā)。
Node.js 開(kāi)發(fā)中一個(gè)更有問(wèn)題的事情就是身份驗(yàn)證的程序很大程度上是開(kāi)發(fā)人員在摸索中完成開(kāi)發(fā)的。事實(shí)上 Express.js 世界中的認(rèn)證解決方案是 Passport,它提供了許多用于身份驗(yàn)證的策略。如果你想要一個(gè)類(lèi)似于 Plataformatec 的 devise 的 Ruby on Rails 的強(qiáng)大的解決方案,你可能會(huì)對(duì) Auth0 感興趣,它是一個(gè)使認(rèn)證成為服務(wù)的開(kāi)創(chuàng)項(xiàng)目。
與 Devise 相比,Passport 只是身份驗(yàn)證中間件,不會(huì)處理任何其他身份驗(yàn)證:這意味著 Node.js 開(kāi)發(fā)人員可能會(huì)定制自己的 API 令牌機(jī)制、密碼重置令牌機(jī)制、用戶(hù)認(rèn)證路由、端點(diǎn)、多種模板語(yǔ)言,因此,有很多教程專(zhuān)門(mén)為你的 Express.js 應(yīng)用程序設(shè)置 Passport,但是幾乎沒(méi)有完全正確的教程,沒(méi)有一個(gè)正確地實(shí)現(xiàn)出 Web 應(yīng)用程序所需的完整堆棧。
請(qǐng)注意: 我不是故意針對(duì)這些教程的開(kāi)發(fā)人員,而是使用他們的身份驗(yàn)證所存在的漏洞后會(huì)讓自己的身份驗(yàn)證系統(tǒng)產(chǎn)生安全問(wèn)題。如果你是教程作者,請(qǐng)?jiān)诟陆坛毯箅S時(shí)與我聯(lián)系。讓 Node/Express 成為開(kāi)發(fā)人員使用的更安全的生態(tài)系統(tǒng)。
錯(cuò)誤一:憑證存儲(chǔ)
讓我們從憑證存儲(chǔ)開(kāi)始。存儲(chǔ)和調(diào)用憑證對(duì)于身份管理來(lái)說(shuō)是非常標(biāo)準(zhǔn)的,而傳統(tǒng)的方法是在你自己的數(shù)據(jù)庫(kù)或應(yīng)用程序中進(jìn)行存儲(chǔ)或者調(diào)用。憑證,作為中間件,簡(jiǎn)單地說(shuō)就是“這個(gè)用戶(hù)可以通過(guò)”或“這個(gè)用戶(hù)不可以通過(guò)”,需要 passport-local 模塊來(lái)處理在你自己的數(shù)據(jù)庫(kù)密碼存儲(chǔ),這個(gè)模塊也是由 Passport.js 作者寫(xiě)的。
在我們進(jìn)入這個(gè)教程的兔子洞之前,請(qǐng)記住 OWASP 的密碼存儲(chǔ)作弊表,它歸結(jié)為“存儲(chǔ)具有獨(dú)特鹽和單向自適應(yīng)成本函數(shù)的高熵密碼”?;蛘呦瓤聪?Coda Hale 的 bcrypt meme,即使有一些爭(zhēng)論。
作為一個(gè)新的 Express.js 和 Passport 用戶(hù),我第一個(gè)要講的地方將是 passport-local 本身的示例代碼,十分感謝 passport 官方提供了一個(gè)可以克隆和擴(kuò)展的 Express.js 4.0 應(yīng)用程序示例,從而我可以克隆和擴(kuò)展。但是,如果我只是拷貝這個(gè)例子,我講不了太多,因?yàn)闆](méi)有數(shù)據(jù)庫(kù)支持的例子,它假設(shè)我只是使用一些設(shè)置好的帳戶(hù)。
沒(méi)關(guān)系,對(duì)吧?這只是一個(gè)內(nèi)聯(lián)網(wǎng)應(yīng)用程序,開(kāi)發(fā)人員說(shuō),下周將分配給我另外四個(gè)項(xiàng)目。當(dāng)然,該示例的密碼不會(huì)以任何方式散列,并且與本示例中的驗(yàn)證邏輯一起存儲(chǔ)在明文中。在這一點(diǎn)上,甚至沒(méi)有考慮到憑證存儲(chǔ)。
讓我們來(lái) google 另一個(gè)使用 passport-local 的教程。我發(fā)現(xiàn)這個(gè)來(lái)自 RisingStack 的一個(gè)叫“Node Hero”系列的快速教程,但從這個(gè)教程中我沒(méi)找到很有用的幫助。他們也在 GitHub 上提供了一個(gè)示例應(yīng)用程序,
但它與官方的問(wèn)題相同。(Ed。8/7/17:RisingStack 現(xiàn)在使用 bcrypt 在他們的教程應(yīng)用。)
接下來(lái),這是第四個(gè)結(jié)果,來(lái)自寫(xiě)于 2015 年的 Google 產(chǎn)出的 express js passport-local 教程。它使用 Mongoose ODM,實(shí)際上從我的數(shù)據(jù)庫(kù)讀取憑據(jù)。 這一個(gè)教程算是比較完整的,包括集成測(cè)試,是的,你可以使用另一個(gè)樣板。但是,Mongoose ODM 也存儲(chǔ)類(lèi)型為 String 的密碼,所以這些密碼也存儲(chǔ)在明文中,只是這一次在 MongoDB 實(shí)例上。(人人都知道 MongoDB 實(shí)例通常是非常安全的)
你可以指責(zé)我擇優(yōu)挑選教程,如果擇優(yōu)挑選意味著從 Google 搜索結(jié)果的第一頁(yè)進(jìn)行選擇,那么你會(huì)是對(duì)的。讓我們選擇 TutsPlus 上更高排名的 passport-local 教程。這一個(gè)更好,因?yàn)?a target="_blank" rel="nofollow">它使用 brypt 的因子為 10 的密碼哈希,并使用 process.nextTick 延遲同步 bcrypt 哈希檢查。Google 的最高成績(jī)來(lái)自 scotch.io 的教程,也使用 成本因子較低為 8 的 bcrypt。這兩個(gè)值都很小,但是 8 真的很小。大多數(shù) bcrypt 庫(kù)現(xiàn)在使用 12。選擇 8 作為成本因子是因?yàn)楣芾韱T帳戶(hù)是十八年前的,這個(gè)因子數(shù)在那時(shí)候就能滿足需求了。
除了密碼存儲(chǔ)之外,這些教程都不會(huì)實(shí)現(xiàn)密碼重置功能,這將作為開(kāi)發(fā)人員的一個(gè)挑戰(zhàn),并且它附帶著自己的陷阱。
錯(cuò)誤二:密碼重置
密碼存儲(chǔ)的一個(gè)姐妹安全問(wèn)題是密碼重置,并且沒(méi)有一個(gè)頂級(jí)的基礎(chǔ)教程解釋了如何使用 Passport 來(lái)完成此操作。你必須另尋他法。
有一千種方法去搞砸這個(gè)問(wèn)題。我見(jiàn)過(guò)的最常見(jiàn)人們重新設(shè)置密碼錯(cuò)誤是:
- 可預(yù)見(jiàn)的令牌。 基于當(dāng)前時(shí)間的令牌是一個(gè)很好的例子。不良偽隨機(jī)數(shù)發(fā)生器產(chǎn)生的令牌相對(duì)好些。
- 存儲(chǔ)不良。 在數(shù)據(jù)庫(kù)中存儲(chǔ)未加密的密碼重置令牌意味著如果數(shù)據(jù)庫(kù)遭到入侵,那些令牌就是明文密碼。使用加密安全的隨機(jī)數(shù)生成器生成長(zhǎng)令牌會(huì)阻止對(duì)重置令牌的遠(yuǎn)程強(qiáng)力攻擊,但不會(huì)阻止本地攻擊。重置令牌是憑據(jù),應(yīng)該這樣處理。
- 無(wú)令牌到期。 令牌如果沒(méi)有到期時(shí)間會(huì)給攻擊者更多的時(shí)間利用重置窗口。
- 無(wú)次要數(shù)據(jù)驗(yàn)證。安全問(wèn)題是重置的事實(shí)上的數(shù)據(jù)驗(yàn)證。當(dāng)然,開(kāi)發(fā)商必須選擇一個(gè)好的安全問(wèn)題。安全問(wèn)題有自己的問(wèn)題。雖然這可能看起來(lái)像安全性過(guò)度,電子郵件地址是你擁有的,而不是你認(rèn)識(shí)的內(nèi)容,并且會(huì)將身份驗(yàn)證因素混合在一起。你的電子郵件地址成為每個(gè)帳戶(hù)的關(guān)鍵,只需將重置令牌發(fā)送到電子郵件。
如果你是第一次接觸這些內(nèi)容,請(qǐng)嘗試 OWASP 的密碼重置工作表。讓我們回到 Node 中看看它為此提供給我們的東西。
我們將轉(zhuǎn)移到 npm 一秒鐘,并重新查找密碼重置,看看是否已有人做到這一點(diǎn)。有一個(gè)已有五年歷史的 package(通常意味著它很棒)。在 Node.js 的時(shí)間軸上,這個(gè)模塊就像是侏羅紀(jì)時(shí)代的,如果我想要雞蛋里挑骨頭,Math.random() 可以在 V8 中預(yù)測(cè),因此它不應(yīng)該用于令牌生成碼。此外,它不使用 Passport,所以我們繼續(xù)前進(jìn)。
Stack Overflow 上獲取不了太多的幫助,因?yàn)橐粋€(gè)名叫 Stormpath 的公司的開(kāi)發(fā)人員喜歡在可以想象到的每一個(gè)跟這個(gè)相關(guān)的的帖子上都插入他們的 IaaS 啟動(dòng)教程。他們的文檔也隨處可見(jiàn),他們也有關(guān)于密碼重置的博客廣告。但是,所有這一切都隨著 Stormpath 的停業(yè)已經(jīng)停止了,它們公司于 2017 年 8 月 17 日完全關(guān)閉。
好的,回到谷歌,這里似乎存在唯一的教程。我們找到了 Google 搜索 express passport 密碼重置的第一個(gè)結(jié)果。還是我們的老朋友 bcrypt。文章中使用了更小的成本因子 5,這遠(yuǎn)遠(yuǎn)低于了現(xiàn)代使用的成本因素。
但是,與其他教程相比,這篇教程相當(dāng)實(shí)用,因?yàn)樗褂?crypto.randomBytes 來(lái)生成真正的隨機(jī)標(biāo)記,如果不使用它們,則會(huì)過(guò)期。然而,上述實(shí)踐中的 #2 和 #4 與這個(gè)全面的教程不符,因此密碼令牌本身容易受到認(rèn)證錯(cuò)誤,憑據(jù)存儲(chǔ)的影響。
幸運(yùn)的是,由于重置到期,這是有限的使用。但是,如果攻擊者通過(guò) BSON 注入對(duì)數(shù)據(jù)庫(kù)中的用戶(hù)對(duì)象進(jìn)行讀取訪問(wèn),或由于配置錯(cuò)誤,可以自由訪問(wèn) Mongo,這些令牌將非常危險(xiǎn)了。攻擊者只需為每個(gè)用戶(hù)發(fā)出密碼重置,從 DB 讀取未加密的令牌,并為用戶(hù)帳戶(hù)設(shè)置自己的密碼,而不必經(jīng)歷使用 GPU 裝備對(duì) bcrypt 散列進(jìn)行的昂貴的字典攻擊過(guò)程。
錯(cuò)誤三:API 令牌
API 令牌是憑據(jù)。它們與密碼或重置令牌一樣敏感。大多數(shù)開(kāi)發(fā)人員都知道這一點(diǎn),并嘗試將他們的 AWS 密鑰、Twitter 秘密等保留在他們胸前,但是這似乎并沒(méi)有轉(zhuǎn)移到被編寫(xiě)的代碼中。
讓我們使用 JSON Web 令牌獲取 API 憑據(jù)。擁有一個(gè)無(wú)狀態(tài)的、可添加黑名單的、可自定義的令牌比十年來(lái)使用的舊 API 密鑰/私密模式更好。也許我們的初級(jí) Node.js 開(kāi)發(fā)人員曾經(jīng)聽(tīng)說(shuō)過(guò) JWT,或者看到過(guò) passport-jwt,并決定實(shí)施 JWT 策略。無(wú)論如何,接觸 JWT 的人都會(huì)或多或少地受到 Node.js 的影響。(尊敬的Thomas Ptacek 會(huì)認(rèn)為 JWT 不好,但恐怕船已經(jīng)在這里航行。)
我們?cè)?Google 上搜索 express js jwt,然后找到 Soni Pandey 的教程使用 Node.js 中的 JWT(JSON Web 令牌)進(jìn)行用戶(hù)驗(yàn)證,。不幸的是,這教程實(shí)際上并不幫助我們,因?yàn)樗鼪](méi)使用憑證,但是當(dāng)我們?cè)谶@里時(shí),我們會(huì)很快注意到憑據(jù)存儲(chǔ)中的錯(cuò)誤:
- 我們將 以明文形式將 JWT 密鑰存儲(chǔ)在存儲(chǔ)庫(kù)中。
- 我們將使用對(duì)稱(chēng)密碼存儲(chǔ)密碼。這意味著我可以獲得加密密鑰,并在發(fā)生違規(guī)時(shí)解密所有密碼。加密密鑰與 JWT 秘密共享。
- 我們將使用 AES-256-CTR 進(jìn)行密碼存儲(chǔ)。我們不應(yīng)該使用 AES 來(lái)啟動(dòng),而且這種操作模式?jīng)]有什么幫助。我不知道為什么選擇這個(gè)特別的模式,但是單一的選擇讓密文具有延展性。
讓我們回到 Google,接著尋找下一個(gè)教程。Scotch,在 passport-local 教程中做了一個(gè)密碼存儲(chǔ)的工作,比如只是忽略他們以前告訴你的東西,并將密碼存儲(chǔ)在明文中。
好吧,我們會(huì)給出一個(gè)簡(jiǎn)短的憑證教程,但這并不能幫助只是拷貝的開(kāi)發(fā)者。因?yàn)楦腥さ氖?,這個(gè)教程將這個(gè) mongoose User 對(duì)象序列化到 JWT 中。
讓我們克隆 Scotch 的這個(gè)資源庫(kù),按照說(shuō)明進(jìn)行運(yùn)行。可以無(wú)視一些來(lái)自 Mongoose 的警告,我們可以輸入 http://localhost:8080/setup 來(lái)創(chuàng)建用戶(hù),然后通過(guò)使用 “Nick Cerminara” 和 “password” 的默認(rèn)憑證調(diào)用 /api/authenticate 拿到令牌。這個(gè)令牌返回并顯示在了 Postman 上。

從 Scotch 教程返回的 JWT 令牌。
請(qǐng)注意,JSON Web 令牌已簽名但未加密。這意味著兩個(gè)時(shí)期之間的大斑點(diǎn)是一個(gè) Base64 編碼對(duì)象??焖俳獯a后,我們得到一些有趣的東西。

我喜歡在明文的密碼中使用令牌。
現(xiàn)在,任何一個(gè)包括存儲(chǔ)在 Mongoose 模型甚至過(guò)期的令牌都有你的密碼。鑒于這個(gè)來(lái)自HTTP,我可以把它從線上找出來(lái)。
下一個(gè)教程怎么樣呢?下一個(gè)教程,針對(duì)初學(xué)者的 Express、Passport 和 JSON Web 令牌(jwt),包含相同的信息泄露漏洞。下篇教程來(lái)自 SlatePeak 的一篇做了同樣的序列化文章。在這一點(diǎn)上,我放棄了閱讀。
錯(cuò)誤四:限速
如上所述,我沒(méi)有在任何這些身份驗(yàn)證教程中找到關(guān)于速率限制或帳戶(hù)鎖定的問(wèn)題。
沒(méi)有速率限制,攻擊者可以執(zhí)行在線字典攻擊,比如運(yùn)行 Burp Intruder 等工具,去獲得獲取訪問(wèn)密碼較弱的帳戶(hù)。帳戶(hù)鎖定還可以通過(guò)在下次登錄時(shí)要求用戶(hù)填寫(xiě)擴(kuò)展登錄信息來(lái)幫助解決此問(wèn)題。
請(qǐng)記住,速率限制還有助于可用性。跨平臺(tái)文件加密工具是一個(gè) CPU 密集型功能,沒(méi)有速率限制功能,使用跨平臺(tái)文件加密工具會(huì)讓?xiě)?yīng)用程序拒絕服務(wù),特別是在 CPU 高數(shù)運(yùn)行時(shí)。比如用戶(hù)注冊(cè)或檢查登錄密碼的多個(gè)請(qǐng)求盡管是輕量級(jí)的 HTTP 的請(qǐng)求,但是會(huì)花費(fèi)服務(wù)器大量的昂貴時(shí)間。
雖然我沒(méi)有教程可以證明這點(diǎn),但 Express 有很多速率限制的技術(shù),例如 express-rate-limit,express-limiter 以及 express-brute。我不能評(píng)價(jià)這些模塊的安全性,甚至沒(méi)有看過(guò)它們;無(wú)論你的負(fù)載平衡用的是什么,通常我推薦在生產(chǎn)中運(yùn)行逆向代理,并允許由 nginx 限制請(qǐng)求處理速率。
身份驗(yàn)證是困難的
我相信這些有錯(cuò)誤的教程開(kāi)發(fā)人員會(huì)辯解說(shuō),“這只是為了解釋基礎(chǔ)!沒(méi)有人會(huì)在生產(chǎn)中這樣做的!”但是,我再三強(qiáng)調(diào)了這是多么錯(cuò)誤。當(dāng)你的教程中的代碼被放在這里時(shí),人們就會(huì)參考并使用你的代碼,畢竟,你比他們有更多的專(zhuān)業(yè)知識(shí)。
如果你是初學(xué)者,請(qǐng)不要信任你的教程。 拷貝教程中的例子可能會(huì)讓你、你的公司和你的客戶(hù)在 Node.js 世界中遇到身份驗(yàn)證問(wèn)題。如果你真的需要強(qiáng)大的生產(chǎn)完善的一體化身份驗(yàn)證庫(kù),那么可以使用更好的手段,比如使用具有更好的穩(wěn)定性,而且更加經(jīng)驗(yàn)證的 Rails/Devise。
Node.js 生態(tài)系統(tǒng)雖然容易接近,但對(duì)需要匆忙編寫(xiě)部署于生產(chǎn)環(huán)境的 Web 應(yīng)用程序的 JavaScript 開(kāi)發(fā)人員來(lái)說(shuō),仍然有很多尖銳的未解決的點(diǎn)。如果你有前端的背景,不知道其他的編程語(yǔ)言,我個(gè)人認(rèn)為,使用 Ruby 是一個(gè)不錯(cuò)的選擇,畢竟站在巨人的肩膀上比從頭開(kāi)始學(xué)習(xí)這些類(lèi)型的東西要容易。
如果你是教程作者,請(qǐng)更新你的教程,特別是樣板代碼。這些代碼將可能被其他人拷貝到生產(chǎn)環(huán)境中的 web 應(yīng)用程序。
如果你是一個(gè) Node.js 的鐵桿使用者,希望你在這篇文章中學(xué)到一些關(guān)于使用用憑證驗(yàn)證身份的知識(shí)。你可能會(huì)遇到什么問(wèn)題。這篇文章中我還沒(méi)有找到完美的方法來(lái)完全避免以上錯(cuò)誤。為你的 Express 應(yīng)用程序增加憑證驗(yàn)證不應(yīng)該是你的工作。應(yīng)該有更好的辦法。