無密碼身份驗證:安全、簡單且部署快速

OneAPM官方技術部落格發表於2016-04-07

Passwordless authentication: Secure, simple, and fast to deploy

【編者按】本文作者為 Florian HeinemannRobert Nyman。Florian 來自 MIT 系統設計與管理學院,專注於複雜的社交技術系統。此前曾在企業軟體領域的多家初創公司工作,之後加入 Airbus,擔任知識與創新管理經理一職。Robert 是 Mozilla Hacks 技術傳道師及編輯。曾就 HTML5,JavaScript 以及 Open Web 發表過多次談話與博文。Robert 堅定地看好 HTML5 與 Open Web,自1995年就開始在 Front End 開發部門研究 Web 技術。

本文系 OneAPM 工程師編譯呈現,以下為正文。

Passwordless(無密碼)是用於 Node.js 程式的一種身份驗證中介軟體,能提高使用者安全水平,同時具備部署簡單、快速的特點。

過去幾個月,對熱衷於 Web 安全與保密性的人來說,著實激動人心:出現了許多了不起的文章討論,還有許多事件,都在提高人們的安全意識。

然而,大多數網站仍在使用最早期的 web 身份驗證方式:使用者名稱與密碼。

儘管使用者名稱密碼這種身份驗證方式的確佔據了一席之地,但如果以為這是所有專案的終極選擇,我們便應該更加謹慎了。我們知道,大多數人在訪問網站時都使用同一套密碼。對於那些缺少安全專家支援的 web 專案,如果使用者在該網站的密碼遭到洩露,那就可能傷及他的 Amazon 賬戶,我們真的要讓使用者承擔這種風險麼?此外,這種經典的身份驗證機制至少存在兩種攻擊角度:登入頁與密碼找回頁。而且,後者的實現往往在匆忙中進行,因而風險更高。

最近,我們看到了許多不錯的點子。筆者尤其對一個直觀而且低技術含量的解決方法感興趣:一次性密碼。這種方法部署快速,攻擊面小,而且不需要 QR codes 或 JavaScript。無論何時,使用者想要登入或使之前的會話失效,都可以通過電子郵件或簡訊息收到一個短時間有效的一次性連結與令牌(token)。如果你想試一試,可以下載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 連結的 header 來源洩露出去。

路徑選擇與身份驗證

下文預設你已經通過 var router = express.Router(); 配置好路徑選擇器。此外,express 文件中也有相應的說明。

你至少需要兩個 URLs,從而: - 展示用於獲取使用者郵箱地址的頁面 - 獲取表格細節(通過 POST 方法)

/* GET: login screen */
router.get('/login', function(req, res) {
res.render('login');
});</p>

/* 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 輸入欄中的內容:

<html>
<body>
    <h1>Login</h1>
    <form action="/sendtoken" method="POST">
        Email:
        <br /><input name="user" type="text">
        <br /><input type="submit" value="Login">
    </form>
</body>

保護網頁

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 允許通過請求物件 req.user 獲取使用者 ID。想要展示或重用此 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 在可用性方面的優勢相當明顯。同樣地,這需要經歷不多的幾個步驟,解釋起來也非常簡單。然而,那些使用者訪問頻繁的網站,以及/或那些要求使用者一週內手動登入幾次的網站(比如 Amazon),經典的身份驗證(或更保險地:雙重驗證)或許更加適合。因為使用者很可能會真的記住他們的密碼,並且意識到好密碼的重要性。

儘管 Passwordless 目前比較穩定,筆者還是非常期待你能在 GitHub 留下評論或一些貢獻,或者在 Twitter:@thesumofall 上提問筆者。

原文地址:https://hacks.mozilla.org/2014/10/passwordless-authentication-secure-simple-and-fast-to-deploy/

OneAPM 助您輕鬆鎖定 Node.js 應用效能瓶頸,通過強大的 Trace 記錄逐層分析,直至鎖定行級問題程式碼。以使用者角度展示系統響應速度,以地域和瀏覽器維度統計使用者使用情況。想閱讀更多技術文章,請訪問 OneAPM 官方部落格

本文轉自 OneAPM 官方部落格

相關文章