Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make OTP delivery independent of 2FA gem #452

Merged
merged 1 commit into from Sep 9, 2016
Merged

Conversation

monfresh
Copy link
Contributor

@monfresh monfresh commented Sep 7, 2016

Why: We should not have to modify the
send_two_factor_authentication_code method in the
two_factor_authentication gem in order to accommodate multiple
delivery mechanisms. This violates the "O" in the SOLID object-oriented
design principles. The "O" stands for Open for extension, Closed
for modification.

How:
We can work around the gem's undesirable design choices by making a few
sensible changes:

  • Make the send_two_factor_authentication_code method a no-op method,
    i.e. it doesn't do anything. This means that even though the gem calls
    this method right after Warden signs the user in, nothing will happen,
    which is what we want.
  • Instead of relying on the send_two_factor_authentication_code
    method and the UserOtpSender class to invoke the proper background
    job, we do this work in the controller, and use the delivery method
    chosen by the user to determine which job to invoke.

This allows us to continue our work without having to fork the gem or
wait for our PRs to be approved and merged. We can work on our code
the way we want and propose changes to the gem in parallel, without
one depending on the other.

Note that these are the smallest changes I could come up with to remove
the dependency on Amos's fork, without changing behavior. Due to
preexisting duplication, new duplication was introduced, but it will be
cleaned up in subsequent PRs, which are part of a larger refactor I am
working on.

# Present the TOTP entry screen to users who are TOTP enabled,
# unless the user explictly selects SMS or voice
current_user.totp_enabled? && !use_sms_or_voice_otp_delivery?
end
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 removed this because a TOTP-enabled user can never select any other option before they land on the show view, which is the only place this method was called. When a TOTP-enabled user signs in, they are automatically asked for their Authenticator app code, with the option to use SMS or voice instead. If they choose one of those options, they go to the otp/send path, which never calls this use_totp? method.

job = "#{current_otp_method.to_s.capitalize}SenderOtpJob".constantize

job.perform_later(confirmation_code, unconfirmed_phone)

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 change adheres to the Open/Closed principle because it allows us to add new delivery options without changing the controller code, whereas before, we would need to add a new elsif statement to the conditional in the controller.

Here's a quick article on SOLID principles: https://robots.thoughtbot.com/back-to-basics-solid

I also highly recommend Sandi Metz's Practical Object-Oriented Design in Ruby: An Agile Primer, which is available on Safari Books Online.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, me likey.

Copy link
Contributor

Choose a reason for hiding this comment

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

Generating references at runtime using .constantize is also pretty horrible though :( It makes it really hard to grep for references and understand there things are using the codebase. I wish there was a nicer solution.

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 a common way of implementing the "Replace Conditional with Polymorphism" refactoring pattern. At least here, you can grep for most of the class name, the SenderOtpJob portion, which is common to all the classes related to sending an OTP, because we have agreed to this naming convention.

If Sandi Metz uses this pattern, it makes it acceptable in my opinion. Search for const_get in this article:

http://www.sandimetz.com/blog/2014/9/9/shape-at-the-bottom-of-all-things

thoughtbot, a well-respected Ruby/Rails firm, also uses this pattern on page 45 of their Ruby Science book.

The other option would be to create a Class or Module that defines a hash that maps the delivery method as submitted by the user to the Active Job class, and then the class can be obtained from the hash. This would not be my first choice though because you have to remember to update the hash every time you add a new delivery method and corresponding Active Job class.

Copy link
Contributor

Choose a reason for hiding this comment

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

I get Sam's point, but using this approach in moderation is not going to create any confusion, imo.

@@ -268,5 +268,4 @@
config.otp_length = 6
config.direct_otp_length = 6
config.direct_otp_valid_for = 5.minutes
config.enable_auto_send_otp = false # disable auto-sending of OTP after authentication
Copy link
Contributor

@amoose amoose Sep 7, 2016

Choose a reason for hiding this comment

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

👍 🔥

**Why**: We should not have to modify the
`send_two_factor_authentication_code` method in the
`two_factor_authentication` gem in order to accommodate multiple
delivery mechanisms. This violates the "O" in the SOLID object-oriented
design principles. The "O" stands for Open for extension, Closed
for modification.

**How**:
We can work around the gem's undesirable design choices by making a few
sensible changes:

- Make the `send_two_factor_authentication_code` method a no-op method,
i.e. it doesn't do anything. This means that even though the gem calls
this method right after Warden signs the user in, nothing will happen,
which is what we want.

- Instead of relying on the `send_two_factor_authentication_code`
method and the `UserOtpSender` class to invoke the proper background
job, we do this work in the controller, and use the delivery method
chosen by the user to determine which job to invoke.

This allows us to continue our work without having to fork the gem or
wait for our PRs to be approved and merged. We can work on our code
the way we want and propose changes to the gem in parallel, without
one depending on the other.

Note that these are the smallest changes I could come up with to remove
the dependency on Amos's fork, without changing behavior. Due to
preexisting duplication, new duplication was introduced, but it will be
cleaned up in subsequent PRs, which are part of a larger refactor I am
working on.
@monfresh
Copy link
Contributor Author

monfresh commented Sep 7, 2016

Comment added. Let me know how it looks.

@monfresh
Copy link
Contributor Author

monfresh commented Sep 8, 2016

Is this good to go now, or still reviewing?

@amoose amoose merged commit b3ec042 into master Sep 9, 2016
@amoose amoose deleted the refactor-otp-delivery branch September 9, 2016 06:14
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.

None yet

3 participants