如何在.NET应用程序中实现Google Authenticator的教程?

61

我正在寻找有关如何在.NET应用程序中使用Google Authenticator的教程。这个教程存在吗?如果存在,我该在哪里找到它?

我了解这可以用于为自己的应用程序添加双因素认证。


2
根据我们的相关主题指南,“**即使一些问题符合上述列出的某个类别,仍然有些问题是不相关的:…询问我们推荐或找到一本书、工具、软件库、教程或其他站外资源的问题都是不相关的...**” - Robert Columbia
6个回答

51
在使用Google Authenticator时,我遇到了这个问题,尤其是Espo贡献的代码。我个人对Java转换为C#的结果不太满意,所以我想分享我的版本。除了大量重构代码之外,我还进行了以下更改:
  • 引入了检查小端字节顺序并在必要时转换为大端字节顺序的功能。
  • 引入了HMAC密钥参数。

有关配置URL格式的更多信息,请参见:https://github.com/google/google-authenticator/wiki/Key-Uri-Format

如果您喜欢,请随意使用,并感谢Espo的初步工作。

using System;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Text;

public class GoogleAuthenticator
{
    const int IntervalLength = 30;
    const int PinLength = 6;
    static readonly int PinModulo = (int)Math.Pow(10, PinLength);
    static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    /// <summary>
    ///   Number of intervals that have elapsed.
    /// </summary>
    static long CurrentInterval
    {
        get
        {
            var ElapsedSeconds = (long)Math.Floor((DateTime.UtcNow - UnixEpoch).TotalSeconds);

            return ElapsedSeconds/IntervalLength;
        }
    }

    /// <summary>
    ///   Generates a QR code bitmap for provisioning.
    /// </summary>
    public byte[] GenerateProvisioningImage(string identifier, byte[] key, int width, int height)
    {
        var KeyString = Encoder.Base32Encode(key);
        var ProvisionUrl = Encoder.UrlEncode(string.Format("otpauth://totp/{0}?secret={1}&issuer=MyCompany", identifier, KeyString));

        var ChartUrl = string.Format("https://chart.apis.google.com/chart?cht=qr&chs={0}x{1}&chl={2}", width, height, ProvisionUrl);
        using (var Client = new WebClient())
        {
            return Client.DownloadData(ChartUrl);
        }
    }

    /// <summary>
    ///   Generates a pin for the given key.
    /// </summary>
    public string GeneratePin(byte[] key)
    {
        return GeneratePin(key, CurrentInterval);
    }

    /// <summary>
    ///   Generates a pin by hashing a key and counter.
    /// </summary>
    static string GeneratePin(byte[] key, long counter)
    {
        const int SizeOfInt32 = 4;

        var CounterBytes = BitConverter.GetBytes(counter);

        if (BitConverter.IsLittleEndian)
        {
            //spec requires bytes in big-endian order
            Array.Reverse(CounterBytes);
        }

        var Hash = new HMACSHA1(key).ComputeHash(CounterBytes);
        var Offset = Hash[Hash.Length - 1] & 0xF;

        var SelectedBytes = new byte[SizeOfInt32];
        Buffer.BlockCopy(Hash, Offset, SelectedBytes, 0, SizeOfInt32);

        if (BitConverter.IsLittleEndian)
        {
            //spec interprets bytes in big-endian order
            Array.Reverse(SelectedBytes);
        }

        var SelectedInteger = BitConverter.ToInt32(SelectedBytes, 0);

        //remove the most significant bit for interoperability per spec
        var TruncatedHash = SelectedInteger & 0x7FFFFFFF;

        //generate number of digits for given pin length
        var Pin = TruncatedHash%PinModulo;

        return Pin.ToString(CultureInfo.InvariantCulture).PadLeft(PinLength, '0');
    }

    #region Nested type: Encoder

    static class Encoder
    {
        /// <summary>
        ///   Url Encoding (with upper-case hexadecimal per OATH specification)
        /// </summary>
        public static string UrlEncode(string value)
        {
            const string UrlEncodeAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

            var Builder = new StringBuilder();

            for (var i = 0; i < value.Length; i++)
            {
                var Symbol = value[i];

                if (UrlEncodeAlphabet.IndexOf(Symbol) != -1)
                {
                    Builder.Append(Symbol);
                }
                else
                {
                    Builder.Append('%');
                    Builder.Append(((int)Symbol).ToString("X2"));
                }
            }

            return Builder.ToString();
        }

        /// <summary>
        ///   Base-32 Encoding
        /// </summary>
        public static string Base32Encode(byte[] data)
        {
            const int InByteSize = 8;
            const int OutByteSize = 5;
            const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

            int i = 0, index = 0;
            var Builder = new StringBuilder((data.Length + 7)*InByteSize/OutByteSize);

            while (i < data.Length)
            {
                int CurrentByte = data[i];
                int Digit;

                //Is the current digit going to span a byte boundary?
                if (index > (InByteSize - OutByteSize))
                {
                    int NextByte;

                    if ((i + 1) < data.Length)
                    {
                        NextByte = data[i + 1];
                    }
                    else
                    {
                        NextByte = 0;
                    }

                    Digit = CurrentByte & (0xFF >> index);
                    index = (index + OutByteSize)%InByteSize;
                    Digit <<= index;
                    Digit |= NextByte >> (InByteSize - index);
                    i++;
                }
                else
                {
                    Digit = (CurrentByte >> (InByteSize - (index + OutByteSize))) & 0x1F;
                    index = (index + OutByteSize)%InByteSize;

                    if (index == 0)
                    {
                        i++;
                    }
                }

                Builder.Append(Base32Alphabet[Digit]);
            }

            return Builder.ToString();
        }
    }

    #endregion
}

1
谢谢您改善我的代码。正如您所注意到的那样,它并不是很漂亮,因为我不想花太多时间来做POC。 - Espo
诚然,我还没有阅读规范,但我打算去做。但是为了快速进行“概念验证”,有人可以告诉我keyidentifier参数的参数吗?听起来“标识符”是出现在Google Authenticator应用程序中代码旁边的名称。但我不确定如何创建密钥。 - Nick Williams
5
“identifier” 是 Google Authenticator 应用中显示的名称,“key” 是一个由随机字节组成的数组,它用作共享密钥。Google Authenticator 应用程序使用该密钥生成 PIN 码,而您的应用程序将使用该密钥验证 PIN 码。请使用来自 System.Security.CryptographyRandomNumberGenerator 生成每个用户帐户的唯一密钥。 - Michael Petito
谢谢!非常有帮助。供其他人参考,快速的谷歌搜索告诉我,密钥最大长度为10个字节,因此key字节数组的最大长度为10。 - Nick Williams
@NickWilliams:你从哪里找到了10字节的限制?我过去在Android上使用Google Authenticator应用程序时,成功地将密钥数组设置为12字节。 - Michael Petito
显示剩余4条评论

28
要使用Google Authenticator添加Google双重身份验证,您需要以下内容:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Security.Cryptography;
using System.Text;
using System.Web.Profile;
using System.Web.Security;
using Google.Authenticator;

获取Google.Authenticator; 请查看此处 https://www.nuget.org/packages/GoogleAuthenticator

现在设置Google身份验证。

TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
var setupInfo = tfa.GenerateSetupCode("Name of the app", "More info ABout the App", "SuperSecretKeyGoesHere", 300 , 300); //the width and height of the Qr Code in pixels

string qrCodeImageUrl = setupInfo.QrCodeSetupImageUrl; //  assigning the Qr code information + URL to string
string manualEntrySetupCode = setupInfo.ManualEntryKey; // show the Manual Entry Key for the users that don't have app or phone
Image1.ImageUrl = qrCodeImageUrl;// showing the qr code on the page "linking the string to image element"
Label1.Text = manualEntrySetupCode; // showing the manual Entry setup code for the users that can not use their phone

你可以将SuperSecretKeyGoesHere更改为任何值,但请确保它有超过10个字符,否则生成的手动输入密钥将无法使用。现在,您可以通过文本框和按钮单击来检查用户输入。
这部分将查看用户输入并查看其是否正确。
string user_enter=TextBox1.Text;
TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
bool isCorrectPIN = tfa.ValidateTwoFactorPIN("SuperSecretKeyGoesHere", user_enter);
if (isCorrectPIN == true)
{
Label2.Text = "i am cool";

}
else
{

Label2.Text = "i am Fool";
}

1
这是一个非常简单的解决方案,并且运行良好。在“line var setupInfo = tfa.GenerateSetupCode”上有一个小错别字,我已经编辑过来修复它了。 - Matthew Proctor
非常容易实现。 - Shaun Vermaak
这与Google没有正式合作关系。是否有一个由Google直接提供的库呢?因为外部方库可能存在安全问题。我搜索了很多,但是没有找到。 - Sameera Udakumbura
警告给 .NET Core 的开发者:这个项目依赖于 System.Drawing,在 Linux 上已知会有问题,并且将在 .NET 6 中被标记为“仅限 Windows”。https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only - Alex from Jitbit

17
在进行了一些研究和测试后,我创建了自己的“概念验证”,展示了如何生成QR图像、从手机上扫描它,然后验证手机上的PIN码是否正确。如果有人想加入,这可能会进一步开发成库。代码可以在此处找到:https://github.com/esp0/googleAuthNet

7
这个问题要求提供一个教程,但我感觉其他答案没有涵盖到。可以在以下链接找到一个教程:

http://www.codeproject.com/Articles/403355/Implementing-Two-Factor-Authentication-in-ASP-NET

这篇教程由Rick Bassham撰写,涵盖了以下信息:
- "什么是双因素身份验证" - "什么是Google Authenticator" - "它是如何工作的"
接着讲解了如何实现以下代码:
- "基于计数器的一次性密码生成" - "基于时间的一次性密码生成"
然后提供了使用Visual Studio 2010的完整教程:
- "我该如何使用它"

3

您可以运行这个简单的控制台应用程序来了解如何验证一次性令牌码。请注意,我们需要先从Nuget包安装库Otp.Net

static string secretKey = "JBSWY3DPEHPK3PXP"; //add this key to your Google Authenticator app  

private static void Main(string[] args)
{
        var bytes = Base32Encoding.ToBytes(secretKey);

        var totp = new Totp(bytes);

        while (true)
        {
            Console.Write("Enter your code from Google Authenticator app: ");
            string userCode = Console.ReadLine();

            //Generate one time token code
            string tokenInApp = totp.ComputeTotp();
            int remainingSeconds = totp.RemainingSeconds();

            if (userCode.Equals(tokenInApp)
                && remainingSeconds > 0)
            {
                Console.WriteLine("Success!");
            }
            else
            {
                Console.WriteLine("Failed. Try again!");
            }
        }
}

我尝试了您示例中的确切解决方案,但Google Authenticator应用程序中的代码不同... - Silencer
@Silencer:你需要确保在Google Authenticator应用程序中使用完全相同的密钥。如果输入错误,它将不同。 - Minh Nguyen
谢谢,我的问题在于我的系统时间不正确,因此生成了不同的代码。 - Silencer
@Silencer:是的,代码中使用了基于时间的方法,因此您需要确保时间系统相同。否则,您可以使用基于计数器的方法,这样它就不会依赖于时间设置。 - Minh Nguyen

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接