/*****************************************************************************/ /// <summary> /// Determine if verification code is valid. Allows for an extra step (30 sec) future grace period for clocks out of sync and a past grace period, i.e. expiration time /// This version of the function is for unit testing only. Use the version with a datetime. /// </summary> /// <param name="verificationCode">Code sent to user's device and received from user</param> /// <param name="secret">Secret generated for code. Should come from storage</param> /// <param name="dtNow">Current data in utc, e.g. DateTime.UtcNow </param> /// <param name="gracePeriod">How long code should last (expires)</param> /// <returns>Returns true if code is valid</returns> public static bool IsValid(int verificationCode, byte[] secret, DateTime dtNow, int expires = TOTPGenerator.Step) { if (verificationCode == TOTPGenerator.GenerateTOTP(secret, dtNow)) { return(true); } // One step grace period in future to account for clock's out of sync if (verificationCode == TOTPGenerator.GenerateTOTP(secret, dtNow.AddSeconds(TOTPGenerator.Step))) { return(true); } // Check up to expiration time var periods = expires / TOTPGenerator.Step; for (var i = 1; i <= periods; ++i) { if (verificationCode == TOTPGenerator.GenerateTOTP(secret, dtNow.AddSeconds(-(TOTPGenerator.Step * i)))) { return(true); } } return(false); }
/*****************************************************************************/ /// <summary> /// Generates a time-based one-time password (TOTP) /// </summary> /// <returns>Returns a tuple with generated secret and code (TOTP). Secret should be persisted (encrypted) for later validation and code should be sent to user's device, e.g. text message</returns> public static (byte[] Secret, int Code) GenerateCode() { var secret = PasswordHasher.CreateSalt(10); // Create a random set of bytes for the secret var code = TOTPGenerator.GenerateTOTP(secret, DateTime.UtcNow); return(secret, code); }