谷歌身份验证器作为公共服务可用吗?

155

是否有公共API可以在自运行的Web应用程序上使用Google Authenticator(双因素身份验证)?

(例如LAMP堆栈)

Google Authenticator

10个回答

123
项目是开源的。我没有使用过它。但它使用了一个文档化的算法(在开源项目页面上列出的RFC中有注明),并且认证器实现支持多个帐户。
实际流程很简单。一次性密码本质上是伪随机数生成器。随机数生成器是一个公式,一旦给定种子或起始数字,就会继续创建随机数流。虽然这些数字可能对彼此来说是随机的,但序列本身是确定性的。因此,一旦你的设备和服务器"同步",则每次按下"下一个号码按钮"时设备创建的随机数将是服务器期望的相同、随机的数字。
安全的一次性密码系统比随机数生成器更复杂,但概念类似。还有其他细节可帮助保持设备和服务器同步。
因此,没有必要让其他人托管认证,比如OAuth。相反,您需要实现与Google为移动设备提供的应用程序兼容的算法。这些软件应该可在开源项目中获得。
根据您的复杂程度,您应该拥有实现此过程的服务器端所需的所有内容,包括OSS项目和RFC。我不知道是否存在特定于您的服务器软件(PHP、Java、.NET等)的实现。
但是,具体而言,您不需要外部服务来处理此过程。

3
另一方面,使用已经存在的、众所周知、易于获取的解决方案,这些解决方案可以在许多不同的移动设备上使用,是非常有益的...(暗示提示) - simpleuser
29
你的意思是短信吗?它速度慢、不可靠且成本高昂。 - Achraf Almouloudi
我写了一篇关于如何在纯Java中实现兼容Google Authenticator/RFC6238的网站双因素认证的博客文章:http://www.asaph.org/2016/04/google-authenticator-2fa-java.html(猖狂广告)。 - Asaph
3
请注意,NIST自2016年8月起不再推荐使用短信作为双因素身份验证方式,尽管成本较低,但它被认为是不安全的。 - TheDPQ

62
该算法在RFC6238中有详细说明,大致步骤如下:
  • 您的服务器向用户提供一个秘密代码,以便安装到Google身份验证器。Google将此作为QR码记录在此处
  • Google身份验证器通过Unix时间和秘密代码的SHA1-HMAC生成6位数字代码(RFC中有更多细节)。
  • 服务器还知道秘密代码/ Unix时间以验证6位数代码。
我尝试在JavaScript中实现该算法,并在这里发布了我的实现。

22

内容很棒,但是使用第一个链接的人应该实施SQL注入预防方法,因为存在一些潜在漏洞。看看第一个链接提出的问题。第二个链接是完美的。 - Septronic

9

1
事后稍晚了一些......我只想跟进一下,提到了一个位于CPAN上的Perl模块:Auth::GoogleAuthenticator (http://search.cpan.org/dist/Auth-GoogleAuthenticator/)。 - DavidO

6

1
我认为你会更喜欢 https://github.com/RobThree/TwoFactorAuth。它基于上述库,但是在更多功能和更清晰的文档方面有了巨大的改进。 - Ema4rl

5

虽然不是LAMP,但如果您使用C#,这是我使用的代码:

代码最初来源于:

https://github.com/kspearrin/Otp.NET

Base32Encoding类来自以下答案:

https://dev59.com/YnRB5IYBdhLWcg3wXmVI#7135008

示例程序:

class Program
{
    static void Main(string[] args)
    {
        var bytes = Base32Encoding.ToBytes("JBSWY3DPEHPK3PXP");

        var totp = new Totp(bytes);

        var result = totp.ComputeTotp();
        var remainingTime = totp.RemainingSeconds();
    }
}

Totp:

public class Totp
{
    const long unixEpochTicks = 621355968000000000L;

    const long ticksToSeconds = 10000000L;

    private const int step = 30;

    private const int totpSize = 6;

    private byte[] key;

    public Totp(byte[] secretKey)
    {
        key = secretKey;
    }

    public string ComputeTotp()
    {
        var window = CalculateTimeStepFromTimestamp(DateTime.UtcNow);

        var data = GetBigEndianBytes(window);

        var hmac = new HMACSHA1();
        hmac.Key = key;
        var hmacComputedHash = hmac.ComputeHash(data);

        int offset = hmacComputedHash[hmacComputedHash.Length - 1] & 0x0F;
        var otp = (hmacComputedHash[offset] & 0x7f) << 24
               | (hmacComputedHash[offset + 1] & 0xff) << 16
               | (hmacComputedHash[offset + 2] & 0xff) << 8
               | (hmacComputedHash[offset + 3] & 0xff) % 1000000;

        var result = Digits(otp, totpSize);

        return result;
    }

    public int RemainingSeconds()
    {
        return step - (int)(((DateTime.UtcNow.Ticks - unixEpochTicks) / ticksToSeconds) % step);
    }

    private byte[] GetBigEndianBytes(long input)
    {
        // Since .net uses little endian numbers, we need to reverse the byte order to get big endian.
        var data = BitConverter.GetBytes(input);
        Array.Reverse(data);
        return data;
    }

    private long CalculateTimeStepFromTimestamp(DateTime timestamp)
    {
        var unixTimestamp = (timestamp.Ticks - unixEpochTicks) / ticksToSeconds;
        var window = unixTimestamp / (long)step;
        return window;
    }

    private string Digits(long input, int digitCount)
    {
        var truncatedValue = ((int)input % (int)Math.Pow(10, digitCount));
        return truncatedValue.ToString().PadLeft(digitCount, '0');
    }

}

Base32编码:

public static class Base32Encoding
{
    public static byte[] ToBytes(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            throw new ArgumentNullException("input");
        }

        input = input.TrimEnd('='); //remove padding characters
        int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
        byte[] returnArray = new byte[byteCount];

        byte curByte = 0, bitsRemaining = 8;
        int mask = 0, arrayIndex = 0;

        foreach (char c in input)
        {
            int cValue = CharToValue(c);

            if (bitsRemaining > 5)
            {
                mask = cValue << (bitsRemaining - 5);
                curByte = (byte)(curByte | mask);
                bitsRemaining -= 5;
            }
            else
            {
                mask = cValue >> (5 - bitsRemaining);
                curByte = (byte)(curByte | mask);
                returnArray[arrayIndex++] = curByte;
                curByte = (byte)(cValue << (3 + bitsRemaining));
                bitsRemaining += 3;
            }
        }

        //if we didn't end with a full byte
        if (arrayIndex != byteCount)
        {
            returnArray[arrayIndex] = curByte;
        }

        return returnArray;
    }

    public static string ToString(byte[] input)
    {
        if (input == null || input.Length == 0)
        {
            throw new ArgumentNullException("input");
        }

        int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
        char[] returnArray = new char[charCount];

        byte nextChar = 0, bitsRemaining = 5;
        int arrayIndex = 0;

        foreach (byte b in input)
        {
            nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
            returnArray[arrayIndex++] = ValueToChar(nextChar);

            if (bitsRemaining < 4)
            {
                nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
                returnArray[arrayIndex++] = ValueToChar(nextChar);
                bitsRemaining += 5;
            }

            bitsRemaining -= 3;
            nextChar = (byte)((b << bitsRemaining) & 31);
        }

        //if we didn't end with a full char
        if (arrayIndex != charCount)
        {
            returnArray[arrayIndex++] = ValueToChar(nextChar);
            while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
        }

        return new string(returnArray);
    }

    private static int CharToValue(char c)
    {
        int value = (int)c;

        //65-90 == uppercase letters
        if (value < 91 && value > 64)
        {
            return value - 65;
        }
        //50-55 == numbers 2-7
        if (value < 56 && value > 49)
        {
            return value - 24;
        }
        //97-122 == lowercase letters
        if (value < 123 && value > 96)
        {
            return value - 97;
        }

        throw new ArgumentException("Character is not a Base32 character.", "c");
    }

    private static char ValueToChar(byte b)
    {
        if (b < 26)
        {
            return (char)(b + 65);
        }

        if (b < 32)
        {
            return (char)(b + 24);
        }

        throw new ArgumentException("Byte is not a value Base32 value.", "b");
    }

}

3

3

不需要网络服务,因为Google Authenticator应用程序不会与Google服务器通信,它只是与您的服务器生成的初始密钥同步(从QR码输入到您的手机),随着时间的推移。


1

-1
对于C#用户,运行这个简单的控制台应用程序以了解如何验证一次性令牌代码。请注意,我们需要先从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!");
            }
        }
}

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