Skip to content

Lock users out temporarily after too many OTPs#1464

Merged
zachmargolis merged 2 commits intomasterfrom
margolis-otp-lockout
Jun 26, 2017
Merged

Lock users out temporarily after too many OTPs#1464
zachmargolis merged 2 commits intomasterfrom
margolis-otp-lockout

Conversation

@zachmargolis
Copy link
Contributor

Why:
Use the database rather than rack-attack

@zachmargolis zachmargolis requested review from amoose and monfresh May 31, 2017 19:17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this method inside handle_valid_otp_delivery_preference since it only applies to OTP, and not TOTP? A global before action means this logic will get exercised when it doesn't need to. It only needs to be exercised when handle_valid_otp_delivery_preference is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great point! Thanks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want to treat this as a MULTI_FACTOR_AUTH_MAX_ATTEMPTS event. I think we need a separate event. Also, we were discussing not locking the user out, and displaying a message such as "Didn't receive your OTP? Please wait a minute before requesting a new one."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you're right, it woudl be confusing with the same lockout event, I'll make another.

I don't recall the discussion about not locking the user out, but happy to change that behavior if we want

@monfresh
Copy link
Contributor

Thanks for taking a first stab at this. Some notes:

  • We want the rate limit to be applied at the phone number level to prevent someone from creating multiple accounts that use the same phone number, and logging into each account, thereby triggering an OTP to the phone number from each account.

  • The logic doesn't seem quite right. The rate limit is meant to be timeboxed. If the rate limit is 3 tries within a findtimeof 5 minutes, if I make an OTP request at 12:01, and then wait until 12:06 to make the second request, my count should be reset back to 1. Instead, my count is set to 2, and my otp_first_sent_at is still set to 12:01.

@monfresh
Copy link
Contributor

Given the complexity of the logic, I propose that we move it to a separate class.

@zachmargolis
Copy link
Contributor Author

Good idea! Will move it to an OtpLimiter class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reset_attempt_count_if_user_no_longer_locked_out method only gets called after a user submits an OTP. It's a separate event than the one we are dealing with here. We need a new method in the OtpLimiter class that resets otp_send_count and otp_first_sent_at when the findtime has expired. Every time a user is about to be sent an OTP, the questions we need to ask are:

  • Has it been more than findtime minutes since the user last received an OTP?
    • YES: set otp_send_count to 1 and otp_first_sent_at to Time.now. Incidentally, this should probably be called otp_last_sent_at.
    • NO: Is otp_send_count >= maxretry?
      • YES: Prevent the user from receiving OTPs for bantime minutes
      • NO: Increment otp_send_count

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. Thanks for pointing this out!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will rename the column and try to implement this, thanks for the details 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I gave this a shot & rebased

@monfresh
Copy link
Contributor

I pushed a new commit with specs that define the desired behavior. Our goal is to make the tests pass. I can work on this.

@monfresh
Copy link
Contributor

I pushed a commit to make one of the tests pass. One more to go.

@monfresh
Copy link
Contributor

I added a new commit that should make all tests pass now. Let me know what you think of this approach. I know there are CC issues. I'll fix them later. There's also an issue that I'll need to write a test for because it's not tested: once a user gets locked out due to sending too many OTPs, when they log back in, the countdown timer is based on the max attempts lockout, which is 5 minutes, which might not necessarily be the same as the bantime for sending too many OTPs. We'll probably need to start tracking the cause of the lock out and display the appropriate content.

@monfresh monfresh force-pushed the margolis-otp-lockout branch from 386163e to fd1dfab Compare June 22, 2017 02:56
Copy link
Contributor Author

@zachmargolis zachmargolis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New changes make sense to me!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add t.timestamps and an index on updated_at? That will let us have a periodic job to delete old records so the table doesn't grow too much

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: double it

@monfresh
Copy link
Contributor

@zachmargolis I addressed your feedback and made the changes to use a single lockout period for both max OTP attempts and requests. I think this PR is in good enough shape for now. Wanna take one last look, please?

@monfresh monfresh force-pushed the margolis-otp-lockout branch 2 times, most recently from 19ba64a to d97e4f0 Compare June 24, 2017 02:58
Copy link
Contributor Author

@zachmargolis zachmargolis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I have some small cleanup suggestions but nothing big

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is identitcal to lockout_time_remaining so I think we can remove it now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think (like you mentioned) there is a small opportunity for a race condition here.

I think that if possible we should take advantage of Rail's first_or_create (as well as a top-level retry), so something like:

def find_or_create_with_phone(phone)
  return if phone.blank? # maybe just raise an error here?
  begin
    OtpRequestsTracker.where(phone_fingerprint: Pii::Fingerprinter.fingerprint(phone)).
      first_or_create(otp_send_count: 0, otp_last_sent_at: Time.zone.now)
  rescue ActiveRecord::RecordNotUnique
    retry # possibly use with_retries gem or add additional logic to only retry once
  end
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Thanks for the suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird. I pushed a 6th commit several minutes ago, but I don't see it on GitHub yet.

@monfresh monfresh force-pushed the margolis-otp-lockout branch from d97e4f0 to f2682b6 Compare June 26, 2017 16:33
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to raise when we're not retrying otherwise this will just return nil and we'll get a NoMethodError rather than a duplicate key error

so like

rescue ActiveRecord::RecordNotUnique
  retry unless ...
  raise
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet! Thanks!!

@monfresh monfresh force-pushed the margolis-otp-lockout branch from 0880ef5 to f65cadd Compare June 26, 2017 18:39
Copy link
Contributor Author

@zachmargolis zachmargolis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@monfresh
Copy link
Contributor

What do you think about the Code Climate similar code issues? Are they worth fixing now, or can we ignore them?

@zachmargolis
Copy link
Contributor Author

This PR has dragged out so long I think it would be worth ignoring the duplicate code, I can follow up with a PR to generate an encrypted setter for one-way hashes the way we do for others

Copy link
Contributor

@monfresh monfresh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@monfresh
Copy link
Contributor

Sounds good. I told CC to approve this PR. Wanna squash and merge?

@zachmargolis
Copy link
Contributor Author

ok! I'll rebase this PR into two commits (one for my changes, one for yours) and then merge this

zachmargolis and others added 2 commits June 26, 2017 15:59
**Why**:
Use the database rather than rack-attack
**Why**:
- Since we don't enforce uniqueness of phone number, a malicious
user could create multiple accounts with the same number and request
OTPs from each account. We want to keep track of the request count
based on the phone number used and then lock out all users who have
that phone number.
- To keep things simple, we use the same lockout period for both
max OTP attempts and max OTP requests
@zachmargolis zachmargolis force-pushed the margolis-otp-lockout branch from f65cadd to af3f5c7 Compare June 26, 2017 20:00
@zachmargolis zachmargolis merged commit b8f61a9 into master Jun 26, 2017
@zachmargolis zachmargolis deleted the margolis-otp-lockout branch June 26, 2017 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants