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.