幫自己的 NET.Core 站台加上 TOTP 雙因子驗證吧

前言

最近公司正在規劃明年要幫資訊系統全面導入 MFA,為此 Infra 部門如火如荼的進行了一連串的可行性評估,並找了一些在做身分認證系統服務的 SaaS 服務廠商來展示系統功能,這才讓我想起之前一直想要幫自己的後台加上手機 OTP 驗證的功能,這篇文章我要記錄一下實作過程的筆記,但在開始之前得先大概了解一下何謂 2FA、MFA、OTP,以及它們之間的關係。


登入因素的定義

網路上很多文章針對「多因素」進行說明,各自的解讀也有點不一致,這邊紀錄文,將針對我個人的理解重新定義,若有不對的地方也請不吝指教的留言給我。

所謂的多因素驗證,必須有兩種以上不同類型的驗證因素,也就是包含兩種不相同的東西,才算,例如,使用密碼當第一步驟,第二步驟驗證電子郵件,因為我認為這都是屬於相同類型的身分驗證因素,因此這類型的就不該歸類為多因素驗證,頂多僅能稱為多步驟驗證,而常見的身分驗證因素則有底下三種類型。

  1. Something you know:你知道的東西,例如密碼、電子郵件或某個問題的答案。
  2. Something you have:你擁有的東西,例如發送具時效性的一次性密碼(OTP)到你的裝置或者硬體金鑰。
  3. Something you are:你個人的生物特徵,例如指紋、人臉或虹膜辨識。

另外針對上述的定義,目前常見的登入身分驗證方式也有底下三種類型。

  1. 單因素認證(Single-Factor Authentication,SFA):這是我們建構應用程式的基本驗證方式,要求使用者僅使用一種類型的身分因素進行身份驗證,大多數情況下是密碼,也是最不安全的一種方式。
  2. 雙因素驗證(Two-Factor Authentication,2FA):登入時會要求使用者提供兩個因素進行身份驗證才能存取帳戶,是一種相對安全驗證。
  3. 多重因素驗證(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 ,並使用 OtpNetQRCoder 這兩個套件來為網站加上雙因此驗證,廢話不多說,直接看底下的程式碼吧。

快速的說明一下 TOTP 的運作原理,它是運用某種神奇的演算法,透過 SecretKey 和目前時間產生隨機的驗證碼,但這個隨機也不是真的那們隨機,只要相同的 SecretKey,在相同的時間內會產生相同的驗證碼,因此我們必須透過兩個步驟來達成 TOTP 雙因素驗證。

  1. 產生並註冊 SecretKey:若會員尚未產生 SecretKey 並綁定 APP 的話,在會員第一次登入成功後,必須產生一組 QRCode,並要求會員透過 Authenticator APP,將 SecretKey 綁訂到該 APP,參考底下程式碼 GenTotpQRCode()
  2. 登入後進行 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 無密碼驗證吧 這篇文章呦。


參考網站


留言