Merged
Conversation
**Why**: These changes were made as part of #2478 in order to make the test in `spec/features/users/sign_in_spec.rb:244` pass, but they masked a bug in `Users::SessionsController`. Moreover, they made the heavily used `find_with_email` method almost 4 times slower. The goal of this particular test is to make sure that the app fails loudly if we mess up the list of keys during key rotation. Preferably, we want to fail as early as possible. Here is the flow that explains why this test was passing before the changes in #2478: 1. User signs in with email and password 2. `Users::SessionsController#create` is called 3. `track_authentication_attempt(auth_params[:email])` is called 4. `user_signed_in_and_not_locked_out?(user)` is called 5. `return false unless current_user` is called 6. Devise calls `current_user`, then Warden calls `UpdateUser` in `config/initializers/session_limitable.rb:9`, which raises `Encryption::EncryptionError` because it is trying to decrypt the `phone` encryptable attribute. With the changes in #2478, `UpdateUser` no longer tries to decrypt the phone, which is a good thing. The flow then continues as follows: 7. `User#need_two_factor_authentication?` is called 8. `two_factor_enabled?` is called and returns true right after it sees that `phone_configuration&.mfa_enabled?` is true. Note that so far, no encrypted attributes have been accessed. 9. After a few more calls, `cache_active_profile` is reached and `cacher.save(auth_params[:password], profile)` is called 10. Within `Pii::Cacher#save`, `stale_attributes?` is called. 11. `user.phone_configuration&.stale_encrypted_phone?` is called, which which raises `Encryption::EncryptionError`. However, `Users::SessionsController#cache_active_profile` rescues this error, and then `profile.deactivate(:encryption_error)` is called, which raises an error because `profile` is `nil`. The reason why we never saw this bug in `SessionsController` is because we haven't had problems rotating keys, and because so far, up until the changes in #2478, `Encryption::EncryptionError` was raised before `cache_active_profile` was reached. For example, before we introduced the PhoneConfiguration table, the error was raised early via `two_factor_enabled?`, which accessed the `phone` encryptable attribute. Similarly, if you change the spec to use a user `:with_authentication_app` instead of `:signed_up`, the test will pass because `two_factor_enabled?` will call `totp_enabled?`, which will try to decrypt the `otp_secret_key`. To make the test pass with an MFA-enabled user, we can wrap the 2 lines (in `cache_active_profile`) after the rescue in an `if profile` block, which we should do anyways. This will then allow the user to sign in, and will then raise the `Encryption::EncryptionError` on the 2FA page when it tries to decrypt the phone number. Ideally, we want to fail as early as possible, but with the current design of `cache_active_profile`, that's not possible because it is coupling actions that only apply to verified users with actions that apply to all users when keys are rotated. In a follow-up PR, I will attempt to extract the key rotation so that decryption attempts fail early and are not rescued.
d47debd to
b2edb1a
Compare
jgsmith-usds
approved these changes
Sep 7, 2018
| profile.deactivate(:encryption_error) | ||
| analytics.track_event(Analytics::PROFILE_ENCRYPTION_INVALID, error: err.message) | ||
| if profile | ||
| profile.deactivate(:encryption_error) |
Contributor
There was a problem hiding this comment.
Interesting that with this change, but no change to the specs, everything passes. I thought about going down this route originally, but the test was looking for a particular decryption error to get thrown, which couldn't come from this code since it gets rescued.
Contributor
Author
There was a problem hiding this comment.
The error comes later. See my commit message.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why: These changes were made as part of #2478 in order to make the
test in
spec/features/users/sign_in_spec.rb:244pass, but theymasked a bug in
Users::SessionsController. Moreover, they made theheavily used
find_with_emailmethod almost 4 times slower. The goal ofthis particular test is to make sure that the app fails loudly if we
mess up the list of keys during key rotation. Preferably, we want to
fail as early as possible.
Here is the flow that explains why this test was passing before the
changes in #2478:
Users::SessionsController#createis calledtrack_authentication_attempt(auth_params[:email])is calleduser_signed_in_and_not_locked_out?(user)is calledreturn false unless current_useris calledcurrent_user, then Warden callsUpdateUserinconfig/initializers/session_limitable.rb:9, which raisesEncryption::EncryptionErrorbecause it is trying to decrypt thephoneencryptable attribute.With the changes in #2478,
UpdateUserno longer tries to decrypt thephone, which is a good thing. The flow then continues as follows:
User#need_two_factor_authentication?is calledtwo_factor_enabled?is called and returns true right after it seesthat
phone_configuration&.mfa_enabled?is true.Note that so far, no encrypted attributes have been accessed.
cache_active_profileis reached andcacher.save(auth_params[:password], profile)is calledPii::Cacher#save,stale_attributes?is called.user.phone_configuration&.stale_encrypted_phone?is called, whichwhich raises
Encryption::EncryptionError.However,
Users::SessionsController#cache_active_profilerescuesthis error, and then
profile.deactivate(:encryption_error)is called,which raises an error because
profileisnil.The reason why we never saw this bug in
SessionsControlleris becausewe haven't had problems rotating keys, and because so far, up until the
changes in #2478,
Encryption::EncryptionErrorwas raised beforecache_active_profilewas reached. For example, before we introducedthe PhoneConfiguration table, the error was raised early via
two_factor_enabled?, which accessed thephoneencryptable attribute.Similarly, if you change the spec to use a user
:with_authentication_appinstead of:signed_up, the test will passbecause
two_factor_enabled?will calltotp_enabled?, which willtry to decrypt the
otp_secret_key.To make the test pass with an MFA-enabled user, we can wrap the 2 lines
(in
cache_active_profile) after the rescue in anif profileblock,which we should do anyways. This will then allow the user to sign in,
and will then raise the
Encryption::EncryptionErroron the 2FA pagewhen it tries to decrypt the phone number.
Ideally, we want to fail as early as possible, but with the current
design of
cache_active_profile, that's not possible because it iscoupling actions that only apply to verified users with actions
that apply to all users when keys are rotated.
In a follow-up PR, I will attempt to extract the key rotation so that
decryption attempts fail early and are not rescued.
Hi! Before submitting your PR for review, and/or before merging it, please
go through the checklists below. These represent the more critical elements
of our code quality guidelines. The rest of the list can be found in
CONTRIBUTING.md
When adding a new controller that requires the user to be fully
authenticated, make sure to add
before_action :confirm_two_factor_authenticatedas the first callback.
Unsafe migrations are implemented over several PRs and over several
deploys to avoid production errors. The strong_migrations gem
will warn you about unsafe migrations and has great step-by-step instructions
for various scenarios.
Indexes were added if necessary. This article provides a good overview
of indexes in Rails.
Verified that the changes don't affect other apps (such as the dashboard)
When relevant, a rake task is created to populate the necessary DB columns
in the various environments right before deploying, taking into account the users
who might not have interacted with this column yet (such as users who have not
set a password yet)
Migrations against existing tables have been tested against a copy of the
production database. See LG-228 Make migrations safer and more resilient #2127 for an example when a migration caused deployment
issues. In that case, all the migration did was add a new column and an index to
the Users table, which might seem innocuous.
The changes are compatible with data that was encrypted with the old code.
GET requests are not vulnerable to CSRF attacks (i.e. they don't change
state or result in destructive behavior).
When adding user data to the session, use the
user_sessionhelperinstead of the
sessionhelper so the data does not persist beyond the user'ssession.
Tests added for this feature/bug
Prefer feature/integration specs over controller specs
When adding code that reads data, write tests for nil values, empty strings,
and invalid inputs.