-
-
Notifications
You must be signed in to change notification settings - Fork 468
Authentication [Clearance Omniauth]
We use clearance for authentication and omniauth for the oauth flow with facebook.
Clearance is pretty small and simple to grasp in a short time examining the code:
https://github.com/thoughtbot/clearance
Since clearance is meant to be "a complete solution" it includes code that we don't want (routes, controllers, views and mailers). But still it's well designed and flexible enough so we can use its core in a clean way, ignoring, more or less, the rest. As far as I researched there is no well-maintained and minimalist alternative. Alternatives and reasons why not:
-
Devise: It's a monster with a huge amount of features we don't want. It's true that, on the other hand, Clearance lacks some feature we do want, but they're simple enough to implement ourselves in 30 LOC. Devise is also a more compact complete-solution (with routes, controllers, views and mailers) which makes it difficult to ignore those parts. In particular, the "devise_for" in routes messes up with our translated routes.
-
Sorcery: It was our previous choice, seemed like the minimalist solution we wanted, but right now: 1) It's not well maintained, 2) Design failures: the oauth approach, using session to store user id, storing the user id, and some others and 3) It's starting to get bloated, e.g. https://github.com/Sorcery/sorcery/issues/32
-
Authlogic: Same as Devise but less monster. The problem is the same as with Devise, it tries too hard to provide a complete and compact solution, which makes changing its default behavior difficult.
-
Any other thing is not maintained.
Apart from reading the Clearance README, those are the important points regarding our usage of it you should read:
- Clearance behavior is shrinked to it's core with the config (initializers/clearance.rb) and the User model redefinitions (see user.rb at the bottom)
- The current user identity is stored in the cookie "remember_token". This matches directly with the attribute User#remember_token. Using this instead of the user's id means we can force the sign out of any user by reseting that token.
- User#confirmation_token is the token used for the alternative identification flows, like "recover my password" or "activate account" in our case. We added the User#confirmation_token_expires_at in order to make this token short-lived, which a feature Clearance doesn't have. We control that explicitly with User#confirmation_token_valid?
- User#password_salt is only kept for historical reasons. The previous sorcery was adding a salt to user's password, which means now for all previous users we can't do Bcrypt.new(password) but instead Bcrypt.new(passowrd + salt). We need to keep that salt in order to let users login. We cannot fix this without changing all user's passwords. This is gonna be here for the rest of our history. Also, salting bcrypt has no sense, whatever!
- User#failed_logins_count and User#lock_expires_at manages the locking account feature after too many failed login attempts. This was a missing feature from clearance, so it's implemented internally.
- User#reset_password_email_sent_at stores the last time a forget password was sent, in order to avoid a malicious third party use this feature massively to spam a recipient.
- User#activated_at stored the time a guest user activated their account.
- User#last_sign_in_ip and User#last_login_at stores information about the last ip and time of login for each users. This is only stored in case someday is needed (tracing issues/bugs), no other reason.
And omniauth is the solution to work with oauth. Read its readme for an intro. We just implemented OauthsController#callback to handle the callback. The logic is as follows:
- If there is a signed in user now, then assign the current given authentication to that user (this is not a feature we offer publicly, but might happen).
- If not,
- If the given authentication (oauth "uid") matches with an existing authentication, then login that user.
- If not,
- Try to issue a sign up using the oauth data. If it succeeds, login that new user. If not, show failure error and go to sign in page.
The sign up scenario might fail if:
- The email provided by oauth is already taken
- Oauth didn't provide an email
Those failure scenarios are poorly handled in the UI right now, don't have an explicit message to the user.
-
Beware that in integration testing, if you mock the current time, the browser (phantom or whatever) will not be mocked. The cookie's expiration time is set by ruby and influenced by the mock, which could result in inconsistent behavior because if you set an expiration date in the past that cookie is discarded. The browser checks expiration against the real current time!
-
For mocking omniauth, it already provide helpers to easy the job: https://github.com/omniauth/omniauth/wiki/Integration-Testing. See
spec/controllers/oauths_controller_spec.rb
. -
For authentication in controller specs (or requests, views, etc.), we have helpers provided by clearance: https://github.com/thoughtbot/clearance/blob/master/lib/clearance/testing/controller_helpers.rb
-
For authentication in integration specs, we have nothing new or special. No magic here. To authenticate a user you need to use the sign in page. The only thing we facilitate in tests for this is using a dumb encryption strategy that doesn't encrypt, so if you have a
user
object in a test, you can know for sure that its password isuser.encrypted_password
. See:spec/support/config/clearance.rb