public SimulatedLoginAttempt(SimulatedAccount account,
            string password,
            bool isFromAttacker,
            bool isGuess,
            IPAddress clientAddress,
            string cookieProvidedByBrowser,
            string mistakeType,
            DateTimeOffset eventTime
        )
        {
            string accountId = account != null ? account.UniqueId : StrongRandomNumberGenerator.Get64Bits().ToString();
            bool isPasswordValid = account != null && account.Password == password;

            Attempt = new LoginAttempt
            {
                UsernameOrAccountId = accountId,
                AddressOfClientInitiatingRequest = clientAddress,
                AddressOfServerThatInitiallyReceivedLoginAttempt = new IPAddress(new byte[] {127, 1, 1, 1}),
                TimeOfAttempt = eventTime,
                Api = "web",
                CookieProvidedByBrowser = cookieProvidedByBrowser
            };
            Password = password;
            IsPasswordValid = isPasswordValid;
            IsFromAttacker = isFromAttacker;
            IsGuess = isGuess;
            MistakeType = mistakeType;
        }
        private TimeSpan DefaultTimeout { get; } = new TimeSpan(0, 0, 0, 0, 500); // FUTURE use configuration value

        /// <summary>
        /// Add a new login attempt via a REST PUT.  If the 
        /// </summary>
        /// <param name="passwordProvidedByClient"></param>
        /// <param name="loginAttempt"></param>
        /// <param name="serversResponsibleForCachingThisLoginAttempt"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task<LoginAttempt> PutAsync(LoginAttempt loginAttempt,
            string passwordProvidedByClient = null,
            List<RemoteHost> serversResponsibleForCachingThisLoginAttempt = null,
            TimeSpan? timeout = null, 
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (serversResponsibleForCachingThisLoginAttempt == null)
            {
                serversResponsibleForCachingThisLoginAttempt = GetServersResponsibleForCachingALoginAttempt(loginAttempt);
            }

            return await RestClientHelper.TryServersUntilOneResponds(
                serversResponsibleForCachingThisLoginAttempt,
                timeout ?? DefaultTimeout,
                async (server, localTimeout) => server.Equals(_localHost)
                    ? await
                        _localLoginAttemptController.LocalPutAsync(loginAttempt, passwordProvidedByClient,
                            serversResponsibleForCachingThisLoginAttempt,
                            onlyUpdateTheInMemoryCacheOfTheLoginAttempt: false,
                            cancellationToken: cancellationToken)
                    : await RestClientHelper.PutAsync<LoginAttempt>(server.Uri,
                        "/api/LoginAttempt/" + Uri.EscapeUriString(loginAttempt.UniqueKey), new Object[]
                        {
                            new KeyValuePair<string, LoginAttempt>("loginAttempt", loginAttempt),
                            new KeyValuePair<string, string>("passwordProvidedByClient", passwordProvidedByClient),
                            new KeyValuePair<string, List<RemoteHost>>("serversResponsibleForCachingThisLoginAttempt", serversResponsibleForCachingThisLoginAttempt)
                        },
                        localTimeout,
                        cancellationToken), cancellationToken);
        }
 public async Task WriteLoginAttemptAsync(LoginAttempt attempt, CancellationToken cancelToken)
 {
     if (LoginAttempts == null)
         return;
     await Task.Run(() =>
     {
         LoginAttempts[attempt.UniqueKey] = attempt;
     }, cancelToken);
 }
 public void Testserilization()
 {
     DateTimeOffset now = DateTimeOffset.Now;
     
     LoginAttempt attempt = new LoginAttempt()
     {
         TimeOfAttempt = now
     };
     string serialized = Newtonsoft.Json.JsonConvert.SerializeObject(attempt);
     LoginAttempt deserialized = Newtonsoft.Json.JsonConvert.DeserializeObject<LoginAttempt>(serialized);
     DateTimeOffset deserializedTimeOfAttempt = deserialized.TimeOfAttempt;
     Assert.Equal(now, deserializedTimeOfAttempt);
 }
 public async Task PutCacheOnlyAsync(LoginAttempt loginAttempt,
     List<RemoteHost> serversResponsibleForCachingThisLoginAttempt,
     TimeSpan? timeout,
     CancellationToken cancellationToken = default(CancellationToken))
 {
     await Task.WhenAll(serversResponsibleForCachingThisLoginAttempt.Where( server => !server.Equals(_localHost)).Select( 
         async server =>
         await RestClientHelper.PutAsync(server.Uri,
             "/api/LoginAttempt/" + Uri.EscapeUriString(loginAttempt.UniqueKey), new Object[]
             {
                 new KeyValuePair<string, LoginAttempt>("loginAttempt", loginAttempt),
                 new KeyValuePair<string, bool>("onlyUpdateTheInMemoryCacheOfTheLoginAttempt", true),
                 new KeyValuePair<string, List<RemoteHost>>("serversResponsibleForCachingThisLoginAttempt",
                     serversResponsibleForCachingThisLoginAttempt),
             },
             timeout,
             cancellationToken)
         ));
 }
示例#6
0
        public void RecordLoginAttempt(LoginAttempt attempt)
        {
            //Record login attempts
            if (attempt.Outcome == AuthenticationOutcome.CredentialsValid ||
                attempt.Outcome == AuthenticationOutcome.CredentialsValidButBlocked)
            {
                // If there was a prior success from the same account, remove it, as we only need to track
                // successes to counter failures and we only counter failures once per account.
                LoginAttempt previousAttemptFromSameAccount =
                    RecentLoginSuccessesAtMostOnePerAccount.FirstOrDefault(la => la.UsernameOrAccountId == attempt.UsernameOrAccountId);
                if (previousAttemptFromSameAccount != null)
                {
                    // We found a prior success from the same account.  Remove it.
                    RecentLoginSuccessesAtMostOnePerAccount.Remove(previousAttemptFromSameAccount);
                }

                RecentLoginSuccessesAtMostOnePerAccount.Add(attempt);
            }
            else
            {
                RecentLoginFailures.Add(attempt);
            }
        }
 public double PopularityBasedThresholdMultiplier_T_multiplier(LoginAttempt loginAttempt)
 {
     return loginAttempt.PasswordsHeightOnBinomialLadder >= BinomialLadderFrequencyThreshdold_T ? 1 : 100;
 }
        public async Task<LoginAttempt> LocalPutAsync(LoginAttempt loginAttempt,
            string passwordProvidedByClient = null,
            List<RemoteHost> serversResponsibleForCachingThisLoginAttempt = null,
            bool onlyUpdateTheInMemoryCacheOfTheLoginAttempt = false,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            string key = loginAttempt.UniqueKey;
            
            bool updateTheLocalCache = true;
            bool updateRemoteCaches = !onlyUpdateTheInMemoryCacheOfTheLoginAttempt;
            bool updateStableStore = !onlyUpdateTheInMemoryCacheOfTheLoginAttempt;

            if (loginAttempt.Outcome == AuthenticationOutcome.Undetermined)
            {
                // The outcome of the loginAttempt is not known.  We need to calculate it
                Task<LoginAttempt> outcomeCalculationTask = null;

                lock (_loginAttemptsInProgress)
                {
                    LoginAttempt existingLoginAttempt;
                    if (_loginAttemptCache.TryGetValue(key, out existingLoginAttempt) &&
                        existingLoginAttempt.Outcome != AuthenticationOutcome.Undetermined)
                    {
                        // Another thread has already performed this PUT operation and determined the
                        // outcome.  There's nothing to do but to take that attempt from the cache
                        // so we can return it.
                        loginAttempt = existingLoginAttempt;
                        updateTheLocalCache = updateRemoteCaches = updateStableStore = false;
                    }
                    else if (_loginAttemptsInProgress.TryGetValue(key, out outcomeCalculationTask))
                    {
                        // Another thread already started this put, and will write the
                        // outcome to stable store.  We need only await the outcome and
                        // let the other thread write the outcome to cache and stable store.
                        updateTheLocalCache = updateRemoteCaches = updateStableStore = false;
                    }
                    else
                    {
                        // This thread will need to perform the outcome calculation, and will place
                        // the result in the cache.  We'll start that task off but await it outside
                        // the lock on _loginAttemptsInProgress so that we can release the lock.
                        _loginAttemptsInProgress[key] = outcomeCalculationTask =
                            DetermineLoginAttemptOutcomeAsync(loginAttempt, passwordProvidedByClient, cancellationToken);
                        // The above call will update the local cache and remove _loginAttemptsInProgress[key]
                        // It's best to do add the LoginAttempt to the local cache there, and not below,
                        // because we want to ensure the value is in the cache before we remove the signal
                        // that no other thread needs to determine the outcome of this LoginAttempt.
                        // As a result, there's no need for us to update the local cache below.
                        updateTheLocalCache = false;
                    }
                }

                // If we need to update the loginAttempt based on the outcome calculation
                // (a Task running DetermineLoginAttemptOutcomeAsync), wait for that task
                // to complete and get the loginAttempt with its outcome.
                if (outcomeCalculationTask != null)
                    loginAttempt = await outcomeCalculationTask;
            }

            // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- helps for clarity
            if (updateTheLocalCache || updateRemoteCaches || updateStableStore)
            {
                WriteLoginAttemptInBackground(loginAttempt,
                    serversResponsibleForCachingThisLoginAttempt,
                    updateTheLocalCache: updateTheLocalCache,
                    updateRemoteCaches: updateRemoteCaches,
                    updateStableStore: updateStableStore,
                    cancellationToken: cancellationToken);
            }

            return loginAttempt;            
        }
 public void PutCacheOnlyBackground(LoginAttempt loginAttempt,
     List<RemoteHost> serversThatCacheThisLoginAttempt,
     TimeSpan? timeout = null,
     CancellationToken cancellationToken = default(CancellationToken))
 {
     Task.Run(() => 
         PutCacheOnlyAsync(loginAttempt, serversThatCacheThisLoginAttempt, timeout, cancellationToken), 
                           cancellationToken);
 }
 public List<RemoteHost> GetServersResponsibleForCachingALoginAttempt(LoginAttempt attempt)
 {
     return GetServersResponsibleForCachingALoginAttempt(attempt.AddressOfClientInitiatingRequest.ToString());
 }
 public double PopularityBasedThresholdMultiplier_T_multiplier(LoginAttempt loginAttempt)
 {
     return(loginAttempt.PasswordsHeightOnBinomialLadder >= BinomialLadderFrequencyThreshdold_T ? 1 : 100);
 }
 public double PopularityBasedPenaltyMultiplier_phi(LoginAttempt loginAttempt)
 {
     return(loginAttempt.PasswordsHeightOnBinomialLadder >= BinomialLadderFrequencyThreshdold_T ? 5 : 1);
 }
        /// <returns></returns>
        public async Task UpdateOutcomeIfIpShouldBeBlockedAsync(
            LoginAttempt loginAttempt,
            IpHistory ip,
            List<RemoteHost> serversResponsibleForCachingTheAccount,
            CancellationToken cancellationToken)
        {
            // Always allow a login if there's a valid device cookie associate with this account
            // FUTURE -- we probably want to do something at the account level to track targetted attacks
            //          against individual accounts and lock them out
            if (loginAttempt.DeviceCookieHadPriorSuccessfulLoginForThisAccount)
                return;

            // Choose a block threshold based on whether the provided password was popular or not.
            // (If the actual password isn't popular, the loginAttempt will be blocked either way.)
            double blockThreshold = loginAttempt.PasswordsPopularityAmongFailedGuesses >= _options.ThresholdAtWhichAccountsPasswordIsDeemedPopular ?
                _options.BlockThresholdPopularPassword : _options.BlockThresholdUnpopularPassword;


            // As we account for successes, we'll want to make sure we never give credit for more than one success
            // per account.  This set tracks the accounts we've already given credit for
            HashSet<string> accountsUsedForSuccessCredit = new HashSet<string>();

            // Start the scoring at zero, with a higher score indicating a greater chance this is a brute-force
            // attack.  (We'll conclude it's a brute force attack if the score goes over the BlockThreshold.)
            double bruteLikelihoodScore = 0;


            // This algoirthm estimates the likelihood that the IP is engaged in a brute force attack and should be
            // blocked by examining login failures from the IP from most-recent to least-recent, adjusting (increasing) the
            // BruteLikelihoodScore to account for each failure based on its type (e.g., we penalize known
            // typos less than other login attempts that use popular password guesses).
            //
            // Successful logins reduce our estimated likelihood that the IP address.
            // We also account for successful logins in reverse chronological order, but do so _lazily_:
            // we only only examine the minimum number of successes needed to take the likelihood score below
            // the block threshold.  We do so because we want there to be a cost to an account of having its
            // successes used to prevent an IP from being blocked, otherwise attackers could use a few fake
            // accounts to intersperse lots of login successes between every failure and never be detected.

            // These counters track how many successes we have stepped through in search of login successes
            // that can be used to offset login failures when accounting for the likelihood the IP is attacking
            int successesWithoutCreditsIndex = 0;
            int successesWithCreditsIndex = 0;

            List<LoginAttempt> copyOfRecentLoginFailures;
            List<LoginAttempt> copyOfRecentLoginSuccessesAtMostOnePerAccount;
            lock (ip.RecentLoginFailures)
            {
                copyOfRecentLoginFailures = 
                    ip.RecentLoginFailures.MostRecentToOldest.ToList();
                copyOfRecentLoginSuccessesAtMostOnePerAccount = 
                    ip.RecentLoginSuccessesAtMostOnePerAccount.MostRecentToOldest.ToList();
            }

            // We step through failures in reverse chronological order (from the 0th element of the sequence on up)
            for (int failureIndex = 0;
                failureIndex < copyOfRecentLoginFailures.Count && bruteLikelihoodScore <= blockThreshold;
                failureIndex++)
            {

                // Get the failure at the index in the sequence.
                LoginAttempt failure = copyOfRecentLoginFailures[failureIndex];

                // Stop tracking failures that are too old in order to forgive IPs that have tranferred to benign owner
                if ((DateTimeOffset.Now - failure.TimeOfAttempt) > _options.ExpireFailuresAfter)
                    break;

                // Increase the brute-force likelihood score based on the type of failure.
                // (Failures that indicate a greater chance of being a brute-force attacker, such as those
                //  using popular passwords, warrant higher scores.)
                switch (failure.Outcome)
                {
                    case AuthenticationOutcome.CredentialsInvalidNoSuchAccount:
                        bruteLikelihoodScore += _options.PenaltyForInvalidAccount *
                                                PopularityPenaltyMultiplier(failure.PasswordsPopularityAmongFailedGuesses);
                        break;
                    case AuthenticationOutcome.CredentialsInvalidIncorrectPasswordTypoLikely:
                        bruteLikelihoodScore += _options.PenaltyForInvalidPasswordPerLoginTypo;
                        break;
                    case AuthenticationOutcome.CredentialsInvalidIncorrectPassword:
                    case AuthenticationOutcome.CredentialsInvalidIncorrectPasswordTypoUnlikely:
                        bruteLikelihoodScore += _options.PenaltyForInvalidPasswordPerLoginRarePassword *
                                                PopularityPenaltyMultiplier(failure.PasswordsPopularityAmongFailedGuesses);
                        break;
                    case AuthenticationOutcome.CredentialsInvalidRepeatedIncorrectPassword:
                        // We ignore repeats of incorrect passwords we've already accounted for
                        // No penalty
                        break;                    
                }

                if (bruteLikelihoodScore > blockThreshold)
                {
                    // The most recent failure took us above the threshold at which we would make the decision to block
                    // this login.  However, there are successes we have yet to account for that might reduce the likelihood score.
                    // We'll account for successes that are more recent than that last failure until we either
                    //    (a) run out of successes, or
                    //    (b) reduce the score below the threshold
                    //        (in which case we'll save any remaining successes to use if we again go over the threshold.)

                    while (bruteLikelihoodScore > blockThreshold &&
                           successesWithCreditsIndex < copyOfRecentLoginSuccessesAtMostOnePerAccount.Count &&
                           copyOfRecentLoginSuccessesAtMostOnePerAccount[successesWithCreditsIndex].TimeOfAttempt >
                           failure.TimeOfAttempt)
                    {
                        // Start with successes for which, on a prior calculation of ShouldBlock, we already removed
                        // a credit from the account that logged in via a call to TryGetCredit.

                        LoginAttempt success =
                            copyOfRecentLoginSuccessesAtMostOnePerAccount[successesWithCreditsIndex];

                        if ( // We have not already used this account to reduce the BruteLikelihoooScore
                             // earlier in this calculation (during this call to ShouldBlock)
                            !accountsUsedForSuccessCredit.Contains(success.UsernameOrAccountId) &&
                            // We HAVE received the credit during a prior recalculation
                            // (during a prior call to ShouldBlock)                        
                            success.HasReceivedCreditForUseToReduceBlockingScore)
                        {
                            // Ensure that we don't count this success more than once
                            accountsUsedForSuccessCredit.Add(success.UsernameOrAccountId);

                            // Reduce the brute-force attack likelihood score to account for this past successful login
                            bruteLikelihoodScore += _options.RewardForCorrectPasswordPerAccount;
                        }
                        successesWithCreditsIndex++;
                    }

                    while (bruteLikelihoodScore > blockThreshold &&
                           successesWithoutCreditsIndex < copyOfRecentLoginSuccessesAtMostOnePerAccount.Count &&
                           copyOfRecentLoginSuccessesAtMostOnePerAccount[successesWithoutCreditsIndex].TimeOfAttempt >
                           failure.TimeOfAttempt)
                    {
                        // If we still are above the threshold, use successes for which we will need to remove a new credit
                        // from the account responsible for the success via TryGetCredit.

                        LoginAttempt success =
                            copyOfRecentLoginSuccessesAtMostOnePerAccount[successesWithoutCreditsIndex];

                        if ( // We have not already used this account to reduce the BruteLikelihoodScore
                             // earlier in this calculation (during this call to ShouldBlock)
                            !accountsUsedForSuccessCredit.Contains(success.UsernameOrAccountId) &&
                            // We have NOT received the credit during a prior recalculation
                            // (during a prior call to ShouldBlock)                        
                            !success.HasReceivedCreditForUseToReduceBlockingScore)
                        {
                            // FUTURE -- We may wnat to parallelize to get rid of the latency.  However, it may well not be worth
                            // worth the added complexity, since requests for credits should rarely (if ever) occur more than
                            // once per login

                            // Reduce credit from the account for the login so that the account cannot be used to generate
                            // an unlimited number of login successes.
                            if (await _userAccountClient.TryGetCreditAsync(success.UsernameOrAccountId, 
                                        serversResponsibleForCachingThisAccount: serversResponsibleForCachingTheAccount,
                                        cancellationToken: cancellationToken))
                            {
                                // There exists enough credit left in the account for us to use this success.

                                // Ensure that we don't count this success more than once
                                accountsUsedForSuccessCredit.Add(success.UsernameOrAccountId);

                                // Reduce the brute-force attack likelihood score to account for this past successful login
                                bruteLikelihoodScore += _options.RewardForCorrectPasswordPerAccount;
                            }

                        }
                        successesWithoutCreditsIndex++;
                    }

                }

                // The brute-force attack likelihood score should never fall below 0, even after a success credit.
                if (bruteLikelihoodScore < 0d)
                    bruteLikelihoodScore = 0d;

                if (bruteLikelihoodScore >= blockThreshold)
                {
                    if (loginAttempt.Outcome == AuthenticationOutcome.CredentialsValid)
                    {
                        loginAttempt.Outcome = AuthenticationOutcome.CredentialsValidButBlocked;
                    }
                    break;
                }

            }
        }
        /// <summary>
        /// Add a LoginAttempt, along the way determining whether that loginAttempt should be allowed
        /// (the user authenticated) or denied.
        /// </summary>
        /// <param name="loginAttempt">The login loginAttempt record to be stored.</param>
        /// <param name="passwordProvidedByClient">The plaintext password provided by the client.</param>
        /// <param name="cancellationToken">To allow this async method to be cancelled.</param>
        /// <returns>If the password is correct and the IP not blocked, returns AuthenticationOutcome.CredentialsValid.
        /// Otherwise, it returns a different AuthenticationOutcome.
        /// The client should not be made aware of any information beyond whether the login was allowed or not.</returns>
        protected async Task<LoginAttempt> DetermineLoginAttemptOutcomeAsync(
            LoginAttempt loginAttempt, 
            string passwordProvidedByClient,
            CancellationToken cancellationToken)
        {   
            // We'll need to know more about the IP making this loginAttempt, so let's get the historical information
            // we've been keeping about it.
            Task<IpHistory> ipHistoryGetTask = _ipHistoryCache.GetAsync(loginAttempt.AddressOfClientInitiatingRequest, cancellationToken);

            List<RemoteHost> serversResponsibleForCachingThisAccount =
                _userAccountClient.GetServersResponsibleForCachingAnAccount(loginAttempt.UsernameOrAccountId);

            // Get a copy of the UserAccount record for the account that the client wants to authenticate as.
            UserAccount account = await _userAccountClient.GetAsync(loginAttempt.UsernameOrAccountId, 
                serversResponsibleForCachingThisAccount: serversResponsibleForCachingThisAccount,
                cancellationToken: cancellationToken);

            if (account == null)
            {
                // This appears to be an loginAttempt to login to a non-existent account, and so all we need to do is
                // mark it as such.  However, since it's possible that users will forget their account names and
                // repeatedly loginAttempt to login to a nonexistent account, we'll want to track whether we've seen
                // this clientsIpHistory/account/password tripple before and note in the outcome if it's a repeat so that.
                // the IP need not be penalized for issuign a query that isn't getting it any information it
                // didn't already have.
                loginAttempt.Outcome = _passwordPopularityTracker.HasNonexistentAccountIpPasswordTripleBeenSeenBefore(
                    loginAttempt.AddressOfClientInitiatingRequest, loginAttempt.UsernameOrAccountId, passwordProvidedByClient)
                    ? AuthenticationOutcome.CredentialsInvalidRepeatedNoSuchAccount
                    : AuthenticationOutcome.CredentialsInvalidNoSuchAccount;
            }
            else
            {
                //
                // This is an loginAttempt to login to a valid (existent) account.
                //

                // Determine whether the client provided a cookie that indicate that it has previously logged
                // into this account successfully---a very strong indicator that it is a client used by the
                // legitimate user and not an unknown client performing a guessing attack.
                loginAttempt.DeviceCookieHadPriorSuccessfulLoginForThisAccount =
                        account.HashesOfDeviceCookiesThatHaveSuccessfullyLoggedIntoThisAccount.Contains(
                            loginAttempt.HashOfCookieProvidedByBrowser);
                
                // Test to see if the password is correct by calculating the Phase2Hash and comparing it with the Phase2 hash
                // in this record
                //
                // First, the expensive (phase1) hash which is used to encrypt the EC public key for this account
                // (which we use to store the encryptions of incorrect passwords)
                byte[] phase1HashOfProvidedPassword = account.ComputePhase1Hash(passwordProvidedByClient);
                // Since we can't store the phase1 hash (it can decrypt that EC key) we instead store a simple (SHA256)
                // hash of the phase1 hash.
                string phase2HashOfProvidedPassword = Convert.ToBase64String(SHA256.Create().ComputeHash((phase1HashOfProvidedPassword)));

                // To determine if the password is correct, compare the phase2 has we just generated (phase2HashOfProvidedPassword)
                // with the one generated from the correct password when the user chose their password (account.PasswordHashPhase2).  
                bool isSubmittedPasswordCorrect = phase2HashOfProvidedPassword == account.PasswordHashPhase2;

                if (isSubmittedPasswordCorrect)
                {
                    // The password is corerct.
                    // While we'll tenatively set the outcome to CredentialsValid, the decision isn't yet final.
                    // Down below we will call UpdateOutcomeIfIpShouldBeBlockedAsync.  If we believe the login was from
                    // a malicous IP that just made a lucky guess, it may be revised to CrendtialsValidButBlocked.
                    loginAttempt.Outcome = AuthenticationOutcome.CredentialsValid;
                }
                else
                {
                    //
                    // The password was invalid.  There's lots of work to do to facilitate future analysis
                    // about why this LoginAttempt failed.

                    // So that we can analyze this failed loginAttempt in the future, we'll store the (phase 2) hash of the 
                    // incorrect password along with the password itself, encrypted with the EcPublicAccountLogKey.
                    // (The decryption key to get the incorrect password plaintext back is encrypted with the
                    //  correct password, so you can't get to the plaintext of the incorrect password if you
                    //  don't already know the correct password.)
                    loginAttempt.Phase2HashOfIncorrectPassword = phase2HashOfProvidedPassword;
                    loginAttempt.EncryptAndWriteIncorrectPassword(passwordProvidedByClient,
                        account.EcPublicAccountLogKey);

                    // Next, if it's possible to declare more about this outcome than simply that the 
                    // user provided the incorrect password, let's do so.
                    // Since users who are unsure of their passwords may enter the same username/password twice, but attackers
                    // don't learn anything from doing so, we'll want to account for these repeats differently (and penalize them less).
                    // We actually have two data structures for catching this: A large sketch of clientsIpHistory/account/password triples and a
                    // tiny LRU cache of recent failed passwords for this account.  We'll check both.

                    // The triple sketch will automatically record that we saw this triple when we check to see if we've seen it before.
                    bool repeatFailureIdentifiedBySketch =
                        _passwordPopularityTracker.HasNonexistentAccountIpPasswordTripleBeenSeenBefore(
                            loginAttempt.AddressOfClientInitiatingRequest, loginAttempt.UsernameOrAccountId,
                            passwordProvidedByClient);

                    bool repeatFailureIdentifiedByAccountHashes =
                        account.PasswordVerificationFailures.Count(failedAttempt =>
                            failedAttempt.Phase2HashOfIncorrectPassword == phase2HashOfProvidedPassword) > 0;

                    loginAttempt.Outcome = (repeatFailureIdentifiedByAccountHashes || repeatFailureIdentifiedBySketch)
                        ? AuthenticationOutcome.CredentialsInvalidRepeatedIncorrectPassword
                        : AuthenticationOutcome.CredentialsInvalidIncorrectPassword;
                }
                
                // If the password is correct, we can decrypt the EcPrivateAccountKey and perform analysis to provide
                // enlightenment into past failures that may help us to evaluate whether they were malicious.  Specifically,
                // we may be able to detrmine if past failures were due to typos.
                if (isSubmittedPasswordCorrect)
                {
                    // Determine if any of the outcomes for login attempts from the client IP for this request were the result of typos,
                    // as this might impact our decision about whether or not to block this client IP in response to its past behaviors.
                    UpdateOutcomesUsingTypoAnalysis(await ipHistoryGetTask,
                        account, passwordProvidedByClient, phase1HashOfProvidedPassword);

                    // In the background, update any outcomes for logins to this account from other IPs, so that if those
                    // IPs loginAttempt to login to any account in the future we can gain insight as to whether those past logins
                    // were typos or non-typos.
                    _userAccountClient.UpdateOutcomesUsingTypoAnalysisInBackground(account.UsernameOrAccountId,
                        passwordProvidedByClient, phase1HashOfProvidedPassword,
                        loginAttempt.AddressOfClientInitiatingRequest,
                        serversResponsibleForCachingThisAccount: serversResponsibleForCachingThisAccount,
                        timeout: DefaultTimeout,
                        cancellationToken: cancellationToken);
                }

                // Get the popularity of the password provided by the client among incorrect passwords submitted in the past,
                // as we are most concerned about frequently-guessed passwords.
                Proportion popularity = _passwordPopularityTracker.GetPopularityOfPasswordAmongFailures(
                    passwordProvidedByClient, isSubmittedPasswordCorrect, confidenceLevel: _options.PopularityConfidenceLevel,
                    minDenominatorForPasswordPopularity: _options.MinDenominatorForPasswordPopularity);
                // When there's little data, we want to make sure the popularity is not overstated because           
                // (e.g., if we've only seen 10 account failures since we started watching, it would not be
                //  appropriate to conclude that something we've seen once before represents 10% of likely guesses.)
                loginAttempt.PasswordsPopularityAmongFailedGuesses =
                    popularity.MinDenominator(_options.MinDenominatorForPasswordPopularity).AsDouble;

                // Preform an analysis of the IPs past beavhior to determine if the IP has been performing so many failed guesses
                // that we disallow logins even if it got the right password.  We call this even when the submitted password is
                // correct lest we create a timing indicator (slower responses for correct passwords) that attackers could use
                // to guess passwords even if we'd blocked their IPs.
                IpHistory ip = await ipHistoryGetTask;
                await UpdateOutcomeIfIpShouldBeBlockedAsync(loginAttempt, ip, serversResponsibleForCachingThisAccount, cancellationToken);

                // Add this LoginAttempt to our history of all login attempts for this IP address.
                ip.RecordLoginAttempt(loginAttempt);

                // Update the account record to incorporate what we've learned as a result of processing this login loginAttempt.
                // If this is a success and there's a cookie, it will update the set of cookies that have successfully logged in
                // to include this one.
                // If it's a failure, it will add this to the list of failures that we may be able to learn about later when
                // we know what the correct password is and can determine if it was a typo.
                _userAccountClient.UpdateForNewLoginAttemptInBackground(loginAttempt,
                    timeout: DefaultTimeout,
                    serversResponsibleForCachingThisAccount: serversResponsibleForCachingThisAccount,
                    cancellationToken: cancellationToken);
            }

            // Mark this task as completed by removing it from the Dictionary of tasks storing loginAttemptsInProgress
            // and by putting the login loginAttempt into our cache of recent login attempts.            
            string key = loginAttempt.UniqueKey;
            _loginAttemptCache.Add(key, loginAttempt);
            lock (_loginAttemptsInProgress)
            {
                _loginAttemptsInProgress.Remove(key);
            }

            // We return the processed login loginAttempt so that the caller can determine its outcome and,
            // in the event that the caller wants to keep a copy of the record, ensure that it has the
            // most up-to-date copy.
            return loginAttempt;
        }
        /// <summary>
        /// Store an updated LoginAttempt to the local cache, remote caches, and in stable store.
        /// </summary>
        /// <param name="loginAttempt">The loginAttempt to write to cache/stable store.</param>
        /// <param name="serversResponsibleForCachingThisLoginAttempt"></param>
        /// <param name="updateTheLocalCache"></param>
        /// <param name="updateRemoteCaches"></param>
        /// <param name="updateStableStore"></param>
        /// <param name="cancellationToken">To allow the async call to be cancelled, such as in the event of a timeout.</param>
        protected async Task WriteLoginAttemptAsync(
            LoginAttempt loginAttempt,
            List<RemoteHost> serversResponsibleForCachingThisLoginAttempt = null,
            bool updateTheLocalCache = true,
            bool updateRemoteCaches = true,
            bool updateStableStore = true,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            Task stableStoreTask = null;

            if (updateTheLocalCache)
            {
                // Write to the local cache on this server
                _loginAttemptCache.Add(loginAttempt.UniqueKey, loginAttempt);
            }

            if (updateStableStore)
            {
                // Write to stable consistent storage (e.g. database) that the system is configured to use
                stableStoreTask = _stableStore.WriteLoginAttemptAsync(loginAttempt, cancellationToken);
            }

            if (updateRemoteCaches)
            {
                // Identify the servers that cache this LoginAttempt and will need their cache entries updated
                if (serversResponsibleForCachingThisLoginAttempt == null)
                {
                    serversResponsibleForCachingThisLoginAttempt =
                        _loginAttemptClient.GetServersResponsibleForCachingALoginAttempt(loginAttempt);
                }

                // Update the cache entries for this LoginAttempt on the remote servers.
                _loginAttemptClient.PutCacheOnlyBackground(loginAttempt,
                    serversResponsibleForCachingThisLoginAttempt,
                    cancellationToken: cancellationToken);
            }

            // If writing to stable store, wait until the write has completed before returning.
            if (stableStoreTask != null)
                await stableStoreTask;
        }
 /// <summary>
 /// Store an updated LoginAttempt to the local cache, remote caches, and in stable store.
 /// </summary>
 /// <param name="attempt">The loginAttempt to write to cache/stable store.</param>
 /// <param name="serversResponsibleForCachingThisLoginAttempt"></param>
 /// <param name="updateTheLocalCache"></param>
 /// <param name="updateRemoteCaches"></param>
 /// <param name="updateStableStore"></param>
 /// <param name="cancellationToken">To allow the async call to be cancelled, such as in the event of a timeout.</param>
 protected void WriteLoginAttemptInBackground(LoginAttempt attempt,
     List<RemoteHost> serversResponsibleForCachingThisLoginAttempt = null,
     bool updateTheLocalCache = true,
     bool updateRemoteCaches = true,
     bool updateStableStore = true,
     CancellationToken cancellationToken = default(CancellationToken))
 {
     Task.Run( () => WriteLoginAttemptAsync(attempt, 
         serversResponsibleForCachingThisLoginAttempt,
         updateTheLocalCache,
         updateRemoteCaches,
         updateStableStore,
         cancellationToken),
         cancellationToken);
 }
 public double PopularityBasedPenaltyMultiplier_phi(LoginAttempt loginAttempt)
 {
     return loginAttempt.PasswordsHeightOnBinomialLadder >= BinomialLadderFrequencyThreshdold_T ? 5 : 1;
 }
示例#18
0
        public async Task<LoginAttempt> AuthenticateAsync(TestConfiguration configuration, string username, string password,
            IPAddress clientAddress = null,
            IPAddress serverAddress = null,
            string api = "web",
            string cookieProvidedByBrowser = null,
            DateTimeOffset? eventTime = null,
            CancellationToken cancellationToken = default(CancellationToken)
            )
        {
            clientAddress = clientAddress ?? new IPAddress(new byte[] {42, 42, 42, 42});
            serverAddress = serverAddress ?? new IPAddress(new byte[] {127, 1, 1, 1});


            LoginAttempt attempt = new LoginAttempt
            {
                UsernameOrAccountId = username,
                AddressOfClientInitiatingRequest = clientAddress,
                AddressOfServerThatInitiallyReceivedLoginAttempt = serverAddress,
                TimeOfAttempt = eventTime ?? DateTimeOffset.Now,
                Api = api,
                CookieProvidedByBrowser = cookieProvidedByBrowser
            };

            return await configuration.MyLoginAttemptClient.PutAsync(attempt, password,
                cancellationToken: cancellationToken);                
        }
 public void UpdateForNewLoginAttemptCacheOnlyInBackground(LoginAttempt attempt,
     List<RemoteHost> serversResponsibleForCachingThisAccount = null,
     TimeSpan? timeout = null,
     CancellationToken cancellationToken = default(CancellationToken))
 {
     Task.Run(() => UpdateForNewLoginAttemptCacheOnlyAsync(
         attempt, serversResponsibleForCachingThisAccount, timeout, cancellationToken),
         cancellationToken);
 }
 public async Task UpdateForNewLoginAttemptCacheOnlyAsync(LoginAttempt attempt,
     List<RemoteHost> serversResponsibleForCachingThisAccount = null,
     TimeSpan? timeout = null,
     CancellationToken cancellationToken = default(CancellationToken))
 {
     if (serversResponsibleForCachingThisAccount == null)
     {
         serversResponsibleForCachingThisAccount =
             GetServersResponsibleForCachingAnAccount(attempt.UsernameOrAccountId);
     }
     await Task.WhenAll(serversResponsibleForCachingThisAccount.Select(server =>
         RestClientHelper.PostAsync(server.Uri,
             "/api/UserAccount/" + Uri.EscapeUriString(attempt.UsernameOrAccountId),
             new Object[]
             {
                 new KeyValuePair<string, LoginAttempt>("attempt", attempt),
                 new KeyValuePair<string, bool>("onlyUpdateTheInMemoryCacheOfTheAccount", true)
             }, timeout, cancellationToken)).ToArray());
 }
        /// <summary>
        /// Update to UserAccount record to incoroprate what we've learned from a LoginAttempt.
        /// 
        /// If the login attempt was successful (Outcome==CrednetialsValid) then we will want to
        /// track the cookie used by the client as we're more likely to trust this client in the future.
        /// If the login attempt was a failure, we'll want to add this attempt to the length-limited
        /// sequence of faield login attempts.
        /// </summary>
        /// <param name="attempt">The attempt to incorporate into the account's records</param>
        /// <param name="serversResponsibleForCachingThisAccount"></param>
        /// <param name="timeout"></param>
        /// <param name="cancellationToken">To allow the async call to be cancelled, such as in the event of a timeout.</param>
        public async Task UpdateForNewLoginAttemptAsync(LoginAttempt attempt,
            List<RemoteHost> serversResponsibleForCachingThisAccount = null,
            TimeSpan? timeout = null,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (serversResponsibleForCachingThisAccount == null)
            {
                serversResponsibleForCachingThisAccount =
                    GetServersResponsibleForCachingAnAccount(attempt.UsernameOrAccountId);
            }

            await RestClientHelper.TryServersUntilOneResponds(
                serversResponsibleForCachingThisAccount,
                timeout ?? DefaultTimeout,
                async (server, innerTimeout) =>
                {
                    if (server.Equals(_localHost))
                        await _localUserAccountController.UpdateForNewLoginAttemptAsync(
                            attempt.UsernameOrAccountId, attempt, false,
                            serversResponsibleForCachingThisAccount,
                            cancellationToken);
                    else
                        await RestClientHelper.PostAsync(server.Uri,
                            "/api/UserAccount/" +
                            Uri.EscapeUriString(attempt.UsernameOrAccountId), new Object[]
                            {
                                new KeyValuePair<string, LoginAttempt>("attempt", attempt),
                                new KeyValuePair<string, bool>(
                                    "onlyUpdateTheInMemoryCacheOfTheAccount", false),
                                new KeyValuePair<string, List<RemoteHost>>(
                                    "serversResponsibleForCachingThisAccount",
                                    serversResponsibleForCachingThisAccount)
                            }, innerTimeout, cancellationToken);
                },
                cancellationToken);
        }