如何使用Microsoft或Google Authenticator添加多重身份验证

目录

介绍

背景

注册二维码

Base32编码器/解码器:


移动应用程序允许您向Microsoft/Google或任何其他TOTP身份验证器应用程序(通过专门生成的二维码)注册您的帐户。成功注册后,验证器应用程序将每30秒生成一个新代码,该代码可用于实现基于MFA的登录。要使其成为完整的MFA,需要将PIN作为前缀添加到应用程序生成的代码中。登录密码或称为密码的人将是PIN+代码。

介绍

基于时间的一次性密码算法(RFC-6238TOTP——基于HMAC的一次性密码算法)的令牌,由MicrosoftGoogle Authenticator移动应用程序等实现。移动应用程序允许您向Microsoft/Google或任何其他TOTP身份验证器应用程序(通过专门生成的二维码)注册您的帐户。成功注册后,验证器应用程序将每30秒生成一个新代码,该代码可用于实现基于MFA的登录。要使其成为完整的MFA,需要将PIN作为前缀添加到应用程序生成的代码中。登录密码或称为密码的人将是PIN +代码。

GitHub存储库

测试部署

背景

为了保护对任何C#JavaC++WindowsLinuxWeb或普通应用程序的访问,MFA是最佳且简单的选择,无需创建您自己的自定义移动应用程序。它完成了场景,即你知道的东西和你拥有的东西。在这里,您知道的是您的PIN,您拥有的是您的移动应用程序和由 Microsoft Authenticator 等身份验证器移动应用程序强制执行的生物功能。

注册二维码

身份验证器应用程序(MicrosoftGoogle)遵循一个标准。但是,只有Google定义了向Authenticator应用程序注册帐户所需的 URI和参数

从逻辑上讲,第一步是能够生成QR码,以便向身份验证器应用程序注册所需的用户。这里的神奇成分是TOTP种子、用户所属的公司/Web应用程序以及用户的UPN或电子邮件地址。

下面的代码使用GUID生成种子(我使用GUID,因为重新生成同一GUID的几率是20亿分之一):

/**
* Converts Hex string to Unsigned Bytes (0 to 256)
*/
public static Byte[] HexToByte(string hexStr)
{
    byte[] bArray = new byte[hexStr.Length / 2];
    for (int i = 0; i < (hexStr.Length / 2); i++)
    {
        byte firstNibble = Byte.Parse(hexStr.Substring((2 * i), 1), System.Globalization.NumberStyles.HexNumber); // [x,y)
        byte secondNibble = Byte.Parse(hexStr.Substring((2 * i) + 1, 1), System.Globalization.NumberStyles.HexNumber);
        int finalByte = (secondNibble) | (firstNibble << 4); // bit-operations only with numbers, not bytes.
        bArray[i] = (byte)finalByte;
    }
    return bArray;
}

/*
 * Generates GUID as a string and remove brackets
 */
public static string getNewId()
{
    string sR = Guid.NewGuid().ToString().ToUpper();
    sR = sR.Replace("{", "");
    sR = sR.Replace("}", "");
    return sR;
}

/*
 * Generates the QR code for authenticator as a base64 encoded svg image
 * You must use something like
 * <img runat="server" id="qrCode" name="qrCode" src="javascript:" alt="Scan this QR code with your mobile application" style="height:300px;width:300px"/>
 */
private void generateQRCode()
{
    //create new key based on hash to be used
    string seed = getNewId() + getNewId();
    seed = seed.Replace("-", "");
    seed = seed.Substring(0, 40);

    byte[] byteSeed = HexToByte(seed);


    //Must save this seed to be able to validate the TOTP
    var KeyString = Base32.ToBase32String(byteSeed);

    string orgDomain = "elogic.synology.me";
    string orgName = "eLogic Builders Inc.";
    string userUPN = "Kashif" + '@' + orgDomain;

    const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&algorithm=SHA1&digits=6&period=30";

    string tokenURI = string.Format(
        AuthenticatorUriFormat,
        HttpUtility.UrlEncode(orgDomain),
        HttpUtility.UrlEncode(userUPN),
        KeyString);


    var qr = QrCode.EncodeText(tokenURI, QrCode.Ecc.High);

    string base64EncodedImage = Convert.ToBase64String(Encoding.UTF8.GetBytes(qr.ToSvgString(4)));

    string imageSrc = "data:image/svg+xml;base64," + base64EncodedImage;

    //Assign image here in your ASP application
    //this.qrCode.Src = imageSrc;
}

必须保存KeyStringTOTP种子)并将其链接到正在验证的用户。相同的种子将用于验证用户输入的TOTP。为了生成QR码,我使用了Net.Codecrete.QrCodeGenerator nuget.org包。这有利于在WindowsLinux上生成QR码(使用Mono框架)。您可以使用适合您应用程序的其他实现。

以下是我用于发送注册的注册链接示例:

当用户点击链接时,QR码生成和注册序列将启动。以下是呈现给用户的内容:

用户使用MicrosoftGoogle或任何其他符合RFC-6238标准的TOTP身份验证器应用程序扫描QR码。应用程序应注册种子和用户的UPN,并应开始生成TOTP

使用下面的 RFC-6238 兼容类,您可以验证生成的TOTP(它是经过少量修改的Microsoft代码示例版本):

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


class SecurityToken
{
    private readonly byte[] _data;

    public SecurityToken(byte[] data)
    {
        _data = (byte[])data.Clone();
    }

    internal byte[] GetDataNoClone()
    {
        return _data;
    }
}

public static class Rfc6238AuthenticationService
{
    private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
    private static readonly Encoding _encoding = new UTF8Encoding(false, true);

    public static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
    {
        // # of 0's = length of pin
        const int mod = 1000000;

        // See https://tools.ietf.org/html/rfc4226
        // We can add an optional modifier
        var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
        var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));

        // Generate DT string
        var offset = hash[hash.Length - 1] & 0xf;
        Debug.Assert(offset + 4 < hash.Length);
        var binaryCode = (hash[offset] & 0x7f) << 24
                         | (hash[offset + 1] & 0xff) << 16
                         | (hash[offset + 2] & 0xff) << 8
                         | (hash[offset + 3] & 0xff);

        return binaryCode % mod;
    }

    private static byte[] ApplyModifier(byte[] input, string modifier)
    {
        if (String.IsNullOrEmpty(modifier))
        {
            return input;
        }

        var modifierBytes = _encoding.GetBytes(modifier);
        var combined = new byte[checked(input.Length + modifierBytes.Length)];
        Buffer.BlockCopy(input, 0, combined, 0, input.Length);
        Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
        return combined;
    }

    // More info: https://tools.ietf.org/html/rfc6238#section-4
    private static ulong GetCurrentTimeStepNumber()
    {
        var delta = DateTime.UtcNow - _unixEpoch;
        return (ulong)(delta.Ticks / _timestep.Ticks);
    }

    private static int GenerateCode(SecurityToken securityToken, string modifier = null)
    {
        if (securityToken == null)
        {
            throw new ArgumentNullException("securityToken");
        }

        // Allow a variance of no greater than 9 minutes in either direction
        var currentTimeStep = GetCurrentTimeStepNumber();
        using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone()))
        {
            return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
        }
    }

    private static bool ValidateCode(SecurityToken securityToken, int code, string modifier = null)
    {
        if (securityToken == null)
        {
            throw new ArgumentNullException("securityToken");
        }

        // Allow a variance of no greater than 9 minutes in either direction
        var currentTimeStep = GetCurrentTimeStepNumber();
        using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone()))
        {
            for (var i = -2; i <= 2; i++)
            {
                var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
                if (computedTotp == code)
                {
                    return true;
                }
            }
        }

        // No match
        return false;
    }
}

以下是可用于验证生成的TOTP的函数:

   public bool CheckTimeBasedOTP_Rfc6238(byte[] byteSeed, string incomingOTP)
    {
        bool bR = false;
        int IntIncomingCode = int.Parse(incomingOTP);

        var hash = new HMACSHA1(byteSeed);
        var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
        var timestep = Convert.ToInt64(unixTimestamp / 30);
        // Allow codes from 90s in each direction (we could make this configurable?)
        for (long i = -2; i <= 2; i++)
        {
            var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
            if (expectedCode == IntIncomingCode)
            {
                bR = true;
                break;
            }
        }

        return bR;
    }

byteSeed是一个字节数组,您可以从Base32编码和保存的种子转换而来。

Base32编码器/解码器:

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

public static class Base32
{
    private static readonly char[] _digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
    private const int _mask = 31;
    private const int _shift = 5;

    private static int CharToInt(char c)
    {
        switch (c)
        {
            case 'A': return 0;
            case 'B': return 1;
            case 'C': return 2;
            case 'D': return 3;
            case 'E': return 4;
            case 'F': return 5;
            case 'G': return 6;
            case 'H': return 7;
            case 'I': return 8;
            case 'J': return 9;
            case 'K': return 10;
            case 'L': return 11;
            case 'M': return 12;
            case 'N': return 13;
            case 'O': return 14;
            case 'P': return 15;
            case 'Q': return 16;
            case 'R': return 17;
            case 'S': return 18;
            case 'T': return 19;
            case 'U': return 20;
            case 'V': return 21;
            case 'W': return 22;
            case 'X': return 23;
            case 'Y': return 24;
            case 'Z': return 25;
            case '2': return 26;
            case '3': return 27;
            case '4': return 28;
            case '5': return 29;
            case '6': return 30;
            case '7': return 31;
        }
        return -1;
    }

    public static byte[] FromBase32String(string encoded)
    {
        if (encoded == null)
            throw new ArgumentNullException(nameof(encoded));

        // Remove whitespace and padding. Note: the padding is used as hint 
        // to determine how many bits to decode from the last incomplete chunk
        // Also, canonicalize to all upper case
        encoded = encoded.Trim().TrimEnd('=').ToUpper();
        if (encoded.Length == 0)
            return new byte[0];

        var outLength = encoded.Length * _shift / 8;
        var result = new byte[outLength];
        var buffer = 0;
        var next = 0;
        var bitsLeft = 0;
        var charValue = 0;
        foreach (var c in encoded)
        {
            charValue = CharToInt(c);
            if (charValue < 0)
                throw new FormatException("Illegal character: `" + c + "`");

            buffer <<= _shift;
            buffer |= charValue & _mask;
            bitsLeft += _shift;
            if (bitsLeft >= 8)
            {
                result[next++] = (byte)(buffer >> (bitsLeft - 8));
                bitsLeft -= 8;
            }
        }

        return result;
    }

    public static string ToBase32String(byte[] data, bool padOutput = false)
    {
        return ToBase32String(data, 0, data.Length, padOutput);
    }

    public static string ToBase32String(byte[] data, int offset, int length, bool padOutput = false)
    {
        if (data == null)
            throw new ArgumentNullException(nameof(data));

        if (offset < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (length < 0)
            throw new ArgumentOutOfRangeException(nameof(length));

        if ((offset + length) > data.Length)
            throw new ArgumentOutOfRangeException();

        if (length == 0)
            return "";

        // SHIFT is the number of bits per output character, so the length of the
        // output is the length of the input multiplied by 8/SHIFT, rounded up.
        // The computation below will fail, so don't do it.
        if (length >= (1 << 28))
            throw new ArgumentOutOfRangeException(nameof(data));

        var outputLength = (length * 8 + _shift - 1) / _shift;
        var result = new StringBuilder(outputLength);

        var last = offset + length;
        int buffer = data[offset++];
        var bitsLeft = 8;
        while (bitsLeft > 0 || offset < last)
        {
            if (bitsLeft < _shift)
            {
                if (offset < last)
                {
                    buffer <<= 8;
                    buffer |= (data[offset++] & 0xff);
                    bitsLeft += 8;
                }
                else
                {
                    int pad = _shift - bitsLeft;
                    buffer <<= pad;
                    bitsLeft += pad;
                }
            }
            int index = _mask & (buffer >> (bitsLeft - _shift));
            bitsLeft -= _shift;
            result.Append(_digits[index]);
        }
        if (padOutput)
        {
            int padding = 8 - (result.Length % 8);
            if (padding > 0) result.Append('=', padding == 8 ? 0 : padding);
        }
        return result.ToString();
    }
}

https://www.codeproject.com/Tips/5384961/How-to-add-a-Multifactor-Authentication-using-Micr

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值