public async Task <GetOneTimeCodeResponse> GetOneTimeCodeAsync(string sendTo, TimeSpan validity, string redirectUrl = null) { var otc = await _oneTimeCodeStore.GetOneTimeCodeAsync(sendTo); if (otc?.ExpiresUTC > DateTime.UtcNow.AddMinutes(2)) { // if they locked the last code, they have to wait until it is almost expired // if they didn't recieve the last code, unfortunately they still need to wait. We can't resent the code // because it is hashed and we don't know what it is. return(new GetOneTimeCodeResponse(GetOneTimeCodeResult.TooManyRequests)); } var rngProvider = new RNGCryptoServiceProvider(); var byteArray = new byte[8]; rngProvider.GetBytes(byteArray); var longCode = BitConverter.ToUInt64(byteArray, 0); var longCodeString = longCode.ToString(); var longCodeHash = GetFastHash(longCodeString); var shortCode = (longCode % 1000000).ToString("000000"); var shortCodeHash = _passwordHashService.HashPassword(shortCode); // a fast hash salted with longCodeHash might be a sufficient alternative otc = new OneTimeCode() { SentTo = sendTo, ShortCodeHash = shortCodeHash, ExpiresUTC = DateTime.UtcNow.Add(validity), LongCodeHash = longCodeHash, RedirectUrl = redirectUrl, FailedAttemptCount = 0, }; await _oneTimeCodeStore.RemoveOneTimeCodeAsync(sendTo); var codeSaved = await _oneTimeCodeStore.AddOneTimeCodeAsync(otc); if (!codeSaved) { return(new GetOneTimeCodeResponse(GetOneTimeCodeResult.ServiceFailure)); } return(new GetOneTimeCodeResponse(GetOneTimeCodeResult.Success) { ShortCode = shortCode, LongCode = longCodeString }); }
public async Task <Response <GetOneTimeCodeResult, GetOneTimeCodeStatus> > GetOneTimeCodeAsync(string sendTo, TimeSpan validity, string redirectUrl = null) { var response = await _oneTimeCodeStore.GetOneTimeCodeAsync(sendTo); var otc = response.Result; if (otc != null && otc.ExpiresUTC > DateTime.UtcNow.AddMinutes(PasswordlessLoginConstants.OneTimeCode.IssueNewCodeIfValidityLessThanXMinutes) && otc.ExpiresUTC < DateTime.UtcNow.AddMinutes(_options.OneTimeCodeValidityMinutes)) { _logger.LogDebug("A once time code exists that has enough time left to use"); // existing code has at least X minutes of validity remaining, so resend it // if more than default validity (e.g. first code sent to new user), user could accidentally // lock the code and not be able to confirm or access the account (terrible UX) if (otc.SentCount >= PasswordlessLoginConstants.OneTimeCode.MaxResendCount) { _logger.LogDebug("The existing one time code has been sent too many times."); return(new Response <GetOneTimeCodeResult, GetOneTimeCodeStatus>( GetOneTimeCodeStatus.Error(_localizer["Too many requests."], GetOneTimeCodeStatusCode.TooManyRequests))); } _logger.LogDebug("Updating the record of how many times the code has been sent"); await _oneTimeCodeStore.UpdateOneTimeCodeSentCountAsync(sendTo, otc.SentCount + 1, redirectUrl); _logger.LogDebug("Returning the still valid code without a client nonce, which can only be delivered once."); return(new Response <GetOneTimeCodeResult, GetOneTimeCodeStatus>( new GetOneTimeCodeResult { ClientNonce = null, // only given out when code is first generated ShortCode = otc.ShortCode, LongCode = otc.LongCode }, GetOneTimeCodeStatus.Success(_localizer["One time code found."]))); } _logger.LogDebug("Generating a new one time code, link, and client nonce."); var rngProvider = new RNGCryptoServiceProvider(); var byteArray = new byte[8]; rngProvider.GetBytes(byteArray); var clientNonceUInt = BitConverter.ToUInt64(byteArray, 0); var clientNonce = clientNonceUInt.ToString(); var clientNonceHash = FastHashService.GetHash(clientNonce, sendTo); rngProvider.GetBytes(byteArray); var longCodeUInt = BitConverter.ToUInt64(byteArray, 0); var longCode = longCodeUInt.ToString(); var shortCode = (longCodeUInt % 1000000).ToString("000000"); otc = new OneTimeCode() { SentTo = sendTo, ClientNonceHash = clientNonceHash, ShortCode = shortCode, ExpiresUTC = DateTime.UtcNow.Add(validity), LongCode = longCode, RedirectUrl = redirectUrl, FailedAttemptCount = 0, SentCount = 1 }; await _oneTimeCodeStore.RemoveOneTimeCodeAsync(sendTo); var codeSavedStatus = await _oneTimeCodeStore.AddOneTimeCodeAsync(otc); if (codeSavedStatus.HasError) { _logger.LogError("Failed to store the code."); return(new Response <GetOneTimeCodeResult, GetOneTimeCodeStatus>( GetOneTimeCodeStatus.Error(_localizer["Failed to save the one time code."], GetOneTimeCodeStatusCode.ServiceFailure))); } return(new Response <GetOneTimeCodeResult, GetOneTimeCodeStatus>( new GetOneTimeCodeResult { ClientNonce = clientNonce, ShortCode = shortCode, LongCode = longCode }, GetOneTimeCodeStatus.Success(_localizer["One time code was generated."]))); }