1

This is how I configured my custom token provider,

 services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                options.Tokens.ProviderMap.Add("CustomEmailConfirmationTokenProvider",
                            new TokenProviderDescriptor(typeof(CustomEmailConfirmationTokenProvider<ApplicationUser>)));
                options.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmationTokenProvider";
            })
                .AddEntityFrameworkStores<IdentityDbContext>()
                .AddTokenProvider<CustomEmailConfirmationTokenProvider<ApplicationUser>>("CustomEmailConfirmationTokenProvider");


           

And this is code for custom token provider class,

 public class CustomEmailConfirmationTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
    {
        public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,IOptions<CustomEmailConfirmationTokenProviderOptions> options
            , ILogger<DataProtectorTokenProvider<TUser>> logger)
            : base(dataProtectionProvider, options,logger)
        { }
    }
    public class CustomEmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
    {
        public CustomEmailConfirmationTokenProviderOptions()
        {
            Name = "CustomEmailConfirmationTokenProvider";
            TokenLifespan = TimeSpan.FromMinutes(1);
        }
    }

this is how I generate user token,

var myToken = await usrManager.GenerateUserTokenAsync(user, "CustomEmailConfirmationTokenProvider", UserManager<object>.ResetPasswordTokenPurpose);

and this is how I verify the token validity using custom provider,

 var isValid = await usrManager.VerifyUserTokenAsync(userinfo, "CustomEmailConfirmationTokenProvider", 
                                   UserManager<object>.ResetPasswordTokenPurpose, model.Token);

The above codes,including setting of TokenLifeSpan are working fine. But the token is generating as a long string as example below,

CfDJ8IvIvIomoPJKkcJtJSNCN4wB6Fp82OPzYvkVaHtBzJBjY9EwOBt2nMg1WudWBTc1giurpRIXhSHeJTe3CLswJEOL7nng9Hd7H/ctDVNSEL5eBnzXVZpvSNmVCvgwIg3cwSNtcjjsYmGFA01EgyEkXXkBZg+jLDiEsKU8YgmaoQd5bOLE3WLopZo2lboG7dOnZv777SMHitbQNJ2SdRyZf2aMAybKAkHnKGIR3ZSyQXRM

I want to change this toke as just 6 digits characters.
Where should I modify to solve this issue?

Steven Sann
  • 478
  • 1
  • 7
  • 27
  • Do you mean you generate 6 digit character token for the identity provider? – Brando Zhang Oct 19 '22 at 05:24
  • @BrandoZhang , Yes, I want to generate 6 digit token, I can generate by DefaultTokenProvider, but unfortunately, DefaultTokenProvider cannot change time out period. The reason why I use customToken provider is to use 6 digit code token and change time span from this token. – Steven Sann Oct 19 '22 at 05:58
  • The actual token being generated by asp.net is only 6-digits. It's the data protection provider that encrypts it to this long string. You can debug the original source to check this behaviour. If you need only 6-digit tokens, look at phone tokens – Ceemah Four Oct 25 '22 at 04:57

1 Answers1

0

If you look at the asp.net sources, you will see that the default token provider is inherited from the TotpSecurityStampBasedTokenProvider DefaultEmailTokenProvider which uses the "D6" format in the GenerateAsync method TotpSecurityStampBasedTokenProvider whereas your custom provider is inherited from the DataProtectorTokenProvider which uses the Base64String format in the GenerateAsync method DataProtectorTokenProvider. I think you can try to change the base class for your custom provider to get a 6 digits token, but the Rfc6238AuthenticationService that is used by the TotpSecurityStampBasedTokenProvider has a hardcoded lifespan of 3 minutes Rfc6238AuthenticationService. For creating a custom Rfc6238AuthenticationService see this answer.

CustomEmailConfirmationTokenProvider

    public class CustomEmailConfirmationTokenProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser> where TUser : class
    {
        private readonly TimeSpan _timeStep;

        public CustomEmailConfirmationTokenProvider()
        {
            // Here you can setup expiration time.
            _timeStep = TimeSpan.FromMinutes(1);
        }

        public override async Task<string> GenerateAsync(
          string purpose,
          UserManager<TUser> manager,
          TUser user)
        {
            if (manager == null)
                throw new ArgumentNullException(nameof(manager));
            byte[] token = await manager.CreateSecurityTokenAsync(user);
            string async = CustomRfc6238AuthenticationService.GenerateCode
                (
                    token,
                    await this.GetUserModifierAsync(purpose, manager, user),
                    _timeStep
                )
                .ToString("D6", (IFormatProvider)CultureInfo.InvariantCulture);
            token = (byte[])null;
            return async;
        }
        
        public override async Task<bool> ValidateAsync(
          string purpose,
          string token,
          UserManager<TUser> manager,
          TUser user)
        {
            if (manager == null)
                throw new ArgumentNullException(nameof(manager));
            int code;
            if (!int.TryParse(token, out code))
                return false;
            byte[] securityToken = await manager.CreateSecurityTokenAsync(user);
            string userModifierAsync = await this.GetUserModifierAsync(purpose, manager, user);
            return securityToken != null && CustomRfc6238AuthenticationService.ValidateCode(
                securityToken,
                code,
                userModifierAsync,
                _timeStep);
        }
        
        // It's just a dummy for inheritance.
        public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
        {
            return Task.FromResult(true);
        }
    }

CustomRfc6238AuthenticationService

    public static class CustomRfc6238AuthenticationService
    {
        private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
        private static readonly Encoding _encoding = new UTF8Encoding(false, true);

        internal static int ComputeTotp(
        HashAlgorithm hashAlgorithm,
            ulong timestepNumber,
            byte[]? modifierBytes)
        {
            // # 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, modifierBytes));

            // 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(Span<byte> input, byte[] modifierBytes)
        {
            var combined = new byte[checked(input.Length + modifierBytes.Length)];
            input.CopyTo(combined);
            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(TimeSpan timeStep)
        {
            var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;

            return (ulong)(delta.Ticks / timeStep.Ticks);
        }
        
        public static int GenerateCode(byte[] securityToken, string? modifier = null, TimeSpan? timeStep = null)
        {
            if (securityToken == null)
            {
                throw new ArgumentNullException(nameof(securityToken));
            }

            // Allow a variance of no greater than time step in either direction
            var currentTimeStep = GetCurrentTimeStepNumber(timeStep ?? _timestep);

            var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null;
            
            using (var hashAlgorithm = new HMACSHA1(securityToken))
            {
                return ComputeTotp(hashAlgorithm, currentTimeStep, modifierBytes);
            }
        }

        public static bool ValidateCode(byte[] securityToken, int code, string? modifier = null, TimeSpan? timeStep = null)
        {
            if (securityToken == null)
            {
                throw new ArgumentNullException(nameof(securityToken));
            }

            // Allow a variance of no greater than time step in either direction
            var currentTimeStep = GetCurrentTimeStepNumber(timeStep ?? _timestep);
            
            using (var hashAlgorithm = new HMACSHA1(securityToken))
            {
                var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null;
                for (var i = -2; i <= 2; i++)
                {
                    var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep - i), modifierBytes);
                    if (computedTotp == code)
                    {
                        return true;
                    }
                }
            }

            // No match
            return false;
        }
    }

Usage

var user = await _userManager.GetUserAsync(User);
if (user != null)
{
    var token = await _userManager.GenerateUserTokenAsync(user, "CustomEmailConfirmationTokenProvider", UserManager<object>.ResetPasswordTokenPurpose);

    var isValid = await _userManager.VerifyUserTokenAsync(user, "CustomEmailConfirmationTokenProvider", UserManager<object>.ResetPasswordTokenPurpose, token);
}

Note: This algorithm has a variance of no greater than time step in either direction.

Weldis
  • 103
  • 1
  • 5