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 CheckOneTimeCodeResult(OneTimeCode otc)
 {
     SentTo      = otc.SentTo;
     RedirectUrl = otc.RedirectUrl;
 }
        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."])));
        }
        private async Task <Response <CheckOneTimeCodeResult, CheckOneTimeCodeStatus> > ExpireTokenAndValidateNonceAsync(OneTimeCode otc, string clientNonce)
        {
            _logger.LogTrace("Validating nonce");

            _logger.LogDebug("Expiring the token so it cannot be used again and so a new token can be generated");
            await _oneTimeCodeStore.ExpireOneTimeCodeAsync(otc.SentTo);

            if (FastHashService.ValidateHash(otc.ClientNonceHash, clientNonce, otc.SentTo))
            {
                _logger.LogDebug("Client nonce was valid");
                return(new Response <CheckOneTimeCodeResult, CheckOneTimeCodeStatus>(
                           new CheckOneTimeCodeResult(otc),
                           CheckOneTimeCodeStatus.Success(_localizer["The one time code was verified."], CheckOneTimeCodeStatusCode.VerifiedWithNonce)));
            }

            _logger.LogDebug("Client nonce was missing or invalid");
            return(new Response <CheckOneTimeCodeResult, CheckOneTimeCodeStatus>(
                       new CheckOneTimeCodeResult(otc),
                       CheckOneTimeCodeStatus.Success(_localizer["The one time code was verified."], CheckOneTimeCodeStatusCode.VerifiedWithoutNonce)));
        }