How failure message works in Passport.js

TD
TD’s note
Published in
11 min readJul 1, 2020
source: http://www.passportjs.org/

歷來學生都不斷地詢問一個問題:

要如何將 Passport 當中的錯誤訊息顯示出來

雖然過去在每一班都回覆過類似的問題,但發現每回覆一次,就會發現新的東西,讓我自己覺得不太安寧。因此,決定透過這篇文章,來結束這場戰鬥 😤

前情提要

Passport.js 是在開發 Node.js 應用程式時,常用到的驗證系統。想了解更多,可以參考以下文章

這篇文章不會講解 Passport 如何使用,將會著重在 failure message 上

先講結論

要呈現 Passport 驗證過程中的錯誤訊息,有以下幾種方法

1. 直接呼叫 req.flash()

在使用 LocalStrategy 的時候 直接呼叫 req.flash() ,這應該是最直接的方法,但是要注意的是,使用這個方法需要 req ,所以上面的 passReqToCallback 一定要設定為 true ,我們才能夠在 verify function 當中傳入並使用 req

2. 開啟 failureFlash 設定

然後可以在 verify function 當中的 done 傳入

// flash type 自動設定為 error
“I’m error message”
// flash type 自動設定為 error。注意這裡的 key 必須為 message
{message: "I’m error message”}
// 手動設定flash type 為 hi。注意後面訊息的 key 必須為 message
{type: 'hi', message: “I’m error message”}

另外一個可以設定的地方在這裡

app.post('/signin', passport.authenticate(
'local',
{ ...
failureFlash: // do something here

}
)

不過這麼一來就比較無法針對驗證過程當中不同的狀況,給予不同的提示訊息(除非搭配使用上面在 done 當中的設定)

設定完之後,錯誤訊息就會透過 connect-flash 套件的協助顯示給使用者

3. 開啟 failureMessage 設定

同樣可以在 verify function 當中的 done 傳入

// 錯誤訊息字串
“I’m error message”
// 錯誤訊息物件注意這裡的 key 必須為 message
{message: "I’m error message”}

之後,開發者可以在 req.session.messages 陣列當中把值取出,在任何地方使用。

以下為原始碼的探索,文長慎入

Start from LocalStrategy

在驗證使用者的過程中,可能會出現 err、找不到該使用者、密碼錯誤的狀況,如果要讓使用者知道實際發生什麼問題,我們可以在使用 LocalStrategy 的時候加入錯誤訊息。

以下是使用 LocalStrategy 的範例程式碼

如果來看 LocalStrategy 函式本身,會是

function Strategy(options, verify) {
...
}

所以在上面的範例程式碼當中,我們傳入了一個物件作為 options,後面接著一個函式做為 verify function

這裡稍微說明 options 裡面的內容。在 LocalStrategy 當中,預設會使用 usernameField 以及 passwordField 這兩筆資料,資料的來源就是登入表單,而且 LocalStrategy 會自動從 req.body 當中解析這兩筆資料出來。

在預設的情況下,LocalStrategy 會去找

  • usernameField: req.body.username or req.query.username
  • passwordField: req.body.password or req.query.password

但如果我們在 options 當中有特別指定名稱,那麼就會採用開發者設定的名稱,譬如

  • usernameField: req.body.email or req.query.email

最後, passReqToCallback的意義是,我們是否要將 req 傳入 verify 函式當中。如果其值為 true,我們可以將 req 傳入 verify 作為第一個參數

// verify function(req, username, password, done) => {...}

談 options 有點離題了,不過在結論當中會提到這一塊,所以特別說明一下。

The last argument of verify function

這裡其實我們比較在意的是, verify function 中的最後參數 done ,你可能也會看到有人寫成 cb ,也就是 callback 的意思。

這裡我們不管怎麼命名,其實都是指向下面這個函式

Ref: https://github.com/jaredhanson/passport/blob/1fd591d5cdcf6516d9eb446216843223c0e020c4/lib/middleware/authenticate.js#L104

這個 varified function 會接收三個參數,第一個參數可以接收 verify function 丟出來 err,第二個參數接收 user 資料,第三個參數為 info。

實際使用方式,舉例來說像是下面四種 cases(當然有更多種方法)

done(err, false)                                                // 1
done(null, false, req.flash('error_messages', 'error')) // 2
done(null, false, { message: 'Email not registered!' }) // 3
done(null, user) // 4

在上面的例子當中,你可以看到 info 有兩種不同的寫法(case 2 & 3),一個是直接呼叫 req.flash ,另外一個是傳入物件。這裡晚點會進一步說明。

不過其實這個 verified function 也沒有做什麼特別的事情,只是根據狀況回傳不同的函式。

  • 如果有接收到 err,二話不說直接回傳 self.error(err) ,也不用管後面的其他參數(對應到上面的 case 1)
  • 如果沒有 err,但是也沒有 user 資料,那就直接回傳 self.fail(info) 。不用管 err 和 user 參數(對應到上面的 case 2 & 3)
  • 如果沒有 err ,同時擁有 user 資料,那麼就開心的回傳 self.success(user, info)(對應到上面的 case 4)

那麼 self 是什麼呢?這裡的 self 指的是 LocalStrategy 本身,不過我們在 LocalStrategy 的原始碼當中,其實找不到 self.error, self.fail, self.success 這三個函式,原因是這些函式是從 Passport 繼承而來的。

因此接下來,我們要回到 LocalStrategy 原始碼,轉戰到 Passport 原始碼

“failures” array in passport

我們可以在 passport/lib/middleware/authenticate.js 裡面找到以下的程式碼

Ref: https://github.com/jaredhanson/passport/blob/1fd591d5cdcf6516d9eb446216843223c0e020c4/lib/middleware/authenticate.js#L293

首先可以看到他接收了兩個參數,不過在 LocalStrategy 裡面我們只傳入了一參數。如果剛剛我們傳入的 info 是個數字,那麼就會被 “歸類” 為 status,如果不是,那麼就會是 challenge。

不管我們傳入什麼,都會被當成一個物件的值,像是

 { challenge: {message: 'xxx'}}

然後被加入到一個叫做 failures 的陣列當中。

Config messages

在同樣的一份檔案裡面,可以找到下面的程式碼,這裡定義了我們如何使用 failures 陣列,以及如何產出錯誤訊息給使用者

首先,會將剛剛放入的 { challenge: {message: ‘xxx’}} 給取出來,接著會另外取出 challenge 的值,也就是 {message: ‘xxx’}

接下來有兩個 if 判斷是,分別判斷 options.failureMessageoptions.failureFlash 的狀態。不過這兩個狀態是從哪裡來的呢?

在我們使用 passport 進行登入驗證的時候,會對於認證後的處理進行設定,譬如下面這樣

app.post('/signin', passport.authenticate(
'local',
{ failureRedirect: '/signin',
failureMessage: true,
failureFlash: true

}
)

這裡我們定義了 failureMessage, failureFalsh 的值為何,也就將會進一步決定剛剛的 challenge 的值 {message: ‘xxx’} 如何被處理。

failureFlash

若值為 true,那麼就會執行下面這段程式碼

讓我來翻譯一下:如果 failureFlash 的值為 true,那麼就會進入下面這段程式碼。但是值為 true 的情況有

  • 給他一個 true
  • 給他一個不為 false 的值,譬如一個字串,或是其他東西

所以如果 failureFlash、也就是後來的 flash 是個字串,那麼就會建立一個物件為

flash = { type: 'error', message: flash }

如果 flash 不是個字串,那麼就去找是否有 flash.type 值,如果沒有,就設定 flash.type 為 error

上面的步驟,基本上就是為了使用 connect-flash 做準備。所以準備好了 type,接下來就要準備 msg (message)。

如果此時 flash 已經是一個物件,且有 flash.message 值,就可以直接採用;若無,則尋找 challenge.message;若無,那麼最後的嘗試就是希望 challenge 本身就是個字串。

最後,在真正送出 req.flash(type, msg) 會再次檢查 msg 是否為字串,若否,則不會送出 flash message。

req.flash() 執行之後,使用者就能夠在前端看到 flash message

failureMessage

若值為 true,那麼就會執行下面這段程式碼

翻譯:如果 failureMessage 是個 boolen 值,那麼 msg 就直接是 challenge.message 或是 challenge。當然這裡 failureMessage 為 true 才會被執行到。

接著,檢查一下 msg 是不是真的是字串,若是,則會把這個字串放到 req.session.messages 這個「陣列」當中。若原本沒有 req.session.messages 這個陣列,就當下創造一個空陣列出來,然後把 msg 放進去 。

之後,開發者就可以利用 req.session.messages ,在任何想要呈現錯誤訊息的地方呈現它

About me

Self-taught and trained in software development knowledge and skills, I am passionate about creating changes through technology.

Find more at Github, LinkedIn, Teaching at ALPHA Camp

--

--