为用户生成 secret, 根据规则创建二维码, 使用微软或是谷歌验证工具扫码登记。验证时由程序生成的 code 与验证工具生成的 code 进行比较。如果一致,则验证通过
https://blog.darkthread.net/blog/mfa-with-ms-authenticator/
<Query Kind="Program">
<NuGetReference>Otp.NET</NuGetReference>
<NuGetReference>QRCoder</NuGetReference>
<Namespace>OtpNet</Namespace>
<Namespace>QRCoder</Namespace>
<Namespace>System.Drawing</Namespace>
</Query>
// 程式會用到兩個 NuGet 套件 - Otp.NET、QRCoder,不囉嗦,直接上程式:
namespace MSAuthenticatorTest
{
class Program
{
static void Main(string[] args)
{
//產生一組 Secret
var secret = KeyGeneration.GenerateRandomKey();
var sd = new SecretData
{
Issuer = "Darkthread",
Label = "TOTP測試",
Secret = Base32Encoding.ToString(secret)
};
//產生 QRCode
//補充說明:此處為求簡便寫成暫存檔以瀏覽器開啟,實際應用時宜全程於記憶體處理資料不落地
//並於網頁顯示完即銷毁,勿以 Email 或其他方傳遞,以降低外流風險
var qrCodeImgFile = System.IO.Path.GetTempFileName() + ".png";
sd.GenQRCode().Save(qrCodeImgFile, System.Drawing.Imaging.ImageFormat.Png);
//使用預設圖片檢視軟體開啟
var p = Process.Start(new ProcessStartInfo()
{
FileName = $"file:///{qrCodeImgFile}",
UseShellExecute = true
});
while (true)
{
Console.WriteLine("輸入一次性密碼進行驗證,或直接按 Enter 結束");
var pwd = Console.ReadLine();
if (string.IsNullOrEmpty(pwd)) break;
Console.WriteLine(" " + sd.ValidateTotp(pwd));
}
}
public class SecretData
{
public string Issuer { get; set; }
public string Label { get; set; }
public string Secret { get; set; }
public string GenQRCodeUrl(){
var secret = Uri.EscapeDataString(Secret);
// var uriString = new OtpUri(OtpType.Totp, secret, this.Label, "ACME Co").ToString();
var uriString = new OtpUri(OtpType.Totp, secret, this.Label, this.Issuer, OtpHashMode.Sha512).ToString();
return uriString;
// $"otpauth://totp/{Label}?issuer={Uri.EscapeDataString(Issuer)}&secret={Uri.EscapeDataString(Secret)}";
}
public Bitmap GenQRCode()
{
var qrcg = new QRCodeGenerator();
var data = qrcg.CreateQrCode(GenQRCodeUrl(), QRCodeGenerator.ECCLevel.Q);
var qrc = new QRCode(data);
return qrc.GetGraphic(20);
}
Totp totpInstance = null;
public string ValidateTotp(string totp)
{
if (totpInstance == null)
{
totpInstance = new Totp(Base32Encoding.ToBytes(this.Secret));
}
long timedWindowUsed;
if (totpInstance.VerifyTotp(totp, out timedWindowUsed))
{
return $"驗證通過 - {timedWindowUsed}";
}
else
{
return "驗證失敗";
}
}
}
}
}