幫自己的 NET.Core 站台加上 TOTP 雙因子驗證吧
前言
最近公司正在規劃明年要幫資訊系統全面導入 MFA,為此 Infra 部門如火如荼的進行了一連串的可行性評估,並找了一些在做身分認證系統服務的 SaaS 服務廠商來展示系統功能,這才讓我想起之前一直想要幫自己的後台加上手機 OTP 驗證的功能,這篇文章我要記錄一下實作過程的筆記,但在開始之前得先大概了解一下何謂 2FA、MFA、OTP,以及它們之間的關係。
登入因素的定義
網路上很多文章針對「多因素」進行說明,各自的解讀也有點不一致,這邊紀錄文,將針對我個人的理解重新定義,若有不對的地方也請不吝指教的留言給我。
所謂的多因素驗證,必須有兩種以上不同類型的驗證因素,也就是包含兩種不相同的東西,才算,例如,使用密碼當第一步驟,第二步驟驗證電子郵件,因為我認為這都是屬於相同類型的身分驗證因素,因此這類型的就不該歸類為多因素驗證,頂多僅能稱為多步驟驗證,而常見的身分驗證因素則有底下三種類型。
- Something you know:你知道的東西,例如密碼、電子郵件或某個問題的答案。
- Something you have:你擁有的東西,例如發送具時效性的一次性密碼(OTP)到你的裝置或者硬體金鑰。
- Something you are:你個人的生物特徵,例如指紋、人臉或虹膜辨識。
另外針對上述的定義,目前常見的登入身分驗證方式也有底下三種類型。
- 單因素認證(Single-Factor Authentication,SFA):這是我們建構應用程式的基本驗證方式,要求使用者僅使用一種類型的身分因素進行身份驗證,大多數情況下是密碼,也是最不安全的一種方式。
- 雙因素驗證(Two-Factor Authentication,2FA):登入時會要求使用者提供兩個因素進行身份驗證才能存取帳戶,是一種相對安全驗證。
- 多重因素驗證(Multi-Factor Authentication,MFA):MFA 與 2FA 類似,但 MFA 可以包含兩個以上的驗證因素,因此它可以提供更高的安全性。
看到這裡,你應該可以理解所有 2FA 都是 MFA,但並非所有 MFA 都是 2FA。
一次性密碼的定義
One-Time Password,簡稱 OTP,它是一種用於身份驗證的技術,其核心是產生一個只能使用一次的密碼,其中最常見的包括:
- TOTP(Time-Based One-Time Password):基於時間的 OTP,根據一個固定的時間間隔生成一個密碼,例如 Google Authenticator 或 Microsoft Authenticator 就是一個常見的 TOTP 實現。
- HOTP(HMAC-Based One-Time Password):基於計數的 OTP,根據一個增加的計數器生成一個密碼,例如 YubiKey 就是一個常見的 HOTP 實現。
開始實做吧
交代了本次實作的基礎知識,現在可以回到 TOTP 的實做了,在這篇文章中,我將使用 NET.Core + Microsoft Authenticator ,並使用 OtpNet、QRCoder 這兩個套件來為網站加上雙因此驗證,廢話不多說,直接看底下的程式碼吧。
快速的說明一下 TOTP 的運作原理,它是運用某種神奇的演算法,透過 SecretKey 和目前時間產生隨機的驗證碼,但這個隨機也不是真的那們隨機,只要相同的 SecretKey,在相同的時間內會產生相同的驗證碼,因此我們必須透過兩個步驟來達成 TOTP 雙因素驗證。
- 產生並註冊 SecretKey:若會員尚未產生 SecretKey 並綁定 APP 的話,在會員第一次登入成功後,必須產生一組 QRCode,並要求會員透過 Authenticator APP,將 SecretKey 綁訂到該 APP,參考底下程式碼 GenTotpQRCode()。
- 登入後進行 2FA 驗證:若會員已完成 APP 綁定後,當會員完成帳號密碼驗證後,需跳出輸入驗證碼的畫面,此時必須拿出 APP 來取得驗證碼進行輸入,參考底下程式碼 ValidateTotp()。
using OtpNet;
using QRCoder;
namespace AuthenticatorDemo.Utility;
public class OtpAuthDomain
{
private Totp? _totp = null;
public string SecretKey { get; }
public OtpAuthDomain(string secretKey)
{
SecretKey = secretKey;
}
public string GenOTPAuthUrl(string label, string issuer, string secretKey, int period = 30, int digits = 6)
{
var url = new OtpUri(OtpType.Totp, secretKey,
user: label,
issuer: issuer,
digits: digits,
period: period
).ToString();
// 會產生以下格式的字串
// otpauth://totp/PrimeEagle%20Studio:Lawrence%20Shen?secret=ZSCMGF6U7H3VYM2QDDU7WNFDFGENTK4K&issuer=PrimeEagle%20Studio&algorithm=SHA1&digits=6&period=30
return url;
}
public byte[] GenTotpQRCode(string url)
{
using QRCodeGenerator qRCodeGenerator = new QRCodeGenerator();
using QRCodeData data = qRCodeGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
using PngByteQRCode qRCode = new PngByteQRCode(data);
byte[] image = qRCode.GetGraphic(10);
return image;
}
public bool ValidateTotp(string code, int period = 30, int digits = 6)
{
if (_totp == null)
_totp = new Totp(Base32Encoding.ToBytes(SecretKey), step: period, totpSize: digits);
if (_totp.VerifyTotp(code, out var timeStepMatched))
{
// 實務上,驗證成功後,可以將 timeStepMatched 存入資料庫,
// 若時間已存在,代表 QR Code 已經被使用過,避免重複使用
return true;
}
return false;
}
}
執行畫面如下圖,其它類似 API 那些不是很重要的 Code 就請自己看一下 Github 內的專案吧,請容許我最後廢話一句。不曉得你有沒有發現要是 SecretKey 若不甚遺失,是不是就代表第二階段驗證形同虛設了。沒錯喔,就是這樣,因此才會有人提到認為 OTP 不算是 2FA 的一種,但這學術問題就交給其它人去煩惱吧,我只是要提醒一下,審選 Authenticator APP 是很重要的,因此若自己沒有開發或者保管能力的話,最好還是選擇 Microsoft 或 Google 這類比較有公信力的驗證器。模擬會員登入後註冊 / 驗證畫面 |
微軟 Authenticator 示意圖 |
本文範例下載 : Github,使用 NetCore 6。
如果還想要為網站實作無密碼登入架構的話,可以參考我先前寫的 別因不安全的密碼讓你的資料裸奔了,了解一下如何為網站加上 WebAuthn 無密碼驗證吧 這篇文章呦。
留言
張貼留言
您好,我是 Lawrence,這裡是我的開發筆記的網誌,如果你對我的文章有任何疑問或者有錯誤的話,歡迎留言讓我知道。