Tips, tricks and security considerations - verachell/Simple-rails-tryout-app-using-devise GitHub Wiki
These are not an exhaustive list of things to know about devise! It's things I came across during the course of this installation.
I'm placing these items in order of greatest importance at the top to least importance at the bottom.
Critically important security consideration if using password reset tokens (which is the default in a devise installation)
In a default installation and with default rails logging behavior, you have a risk of the password reset tokens being exposed in plaintext in logging. This is documented in the official devise Wiki at https://github.com/heartcombo/devise?tab=readme-ov-file#password-reset-tokens-and-rails-logs To sum up, password reset tokens are stored in the database as a digest, not as plain text, but need to be careful with default logging behavior in rails which can expose plaintext of these. Gives a non-zero risk of password reset tokens being stolen, but this can be mitigated by changing the default production logger level
Default behavior of forgot password or didn't receive unlock instructions will say if email isn't found
If we do forgot password
on an email that's not in the database, it says email not found. If we do it on an email that IS in the database, it seems to go ok. So you can tell if the email is or isn't in the database on a default install. The same situation applies to if we do didn't receive unlock instructions
. Therefore, you probably want to mitigate this in a default install (I haven't looked into how)
To set up a user manually - for example if you don't want users being registerable
You can set up a user manually in the rails console covered in a tutorial at https://dev.to/kevinluo201/introduction-to-devise-modules-and-enable-all-of-them-4p25#database_authenticatable You would do it like this:
irb(main):002> myuser = User.new(email: '[email protected]')
=> #<User id: nil, email: "[email protected]", created_at: nil, updated_at:...
add password
irb(main):003> myuser.password = 'Catdog33'
=> "Catdog33"
save user to database
irb(main):004> myuser.save!
TRANSACTION (0.4ms) begin transaction
User Exists? (18.8ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? ["email", "[email protected]"], ["LIMIT", 1](/verachell/Simple-rails-tryout-app-using-devise/wiki/"email",-"[email protected]"],-["LIMIT",-1)
User Create (1.1ms) INSERT INTO "users" ("email", "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at", "failed_attempts", "unlock_token", "locked_at", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING "id" ["email", "[email protected]"], ["encrypted_password", "[FILTERED]"], ["reset_password_token", "[FILTERED]"], ["reset_password_sent_at", "[FILTERED]"], ["remember_created_at", nil], ["failed_attempts", 0], ["unlock_token", "[FILTERED]"], ["locked_at", nil], ["created_at", "2024-03-20 18:59:15.403932"], ["updated_at", "2024-03-20 18:59:15.403932"](/verachell/Simple-rails-tryout-app-using-devise/wiki/"email",-"[email protected]"],-["encrypted_password",-"[FILTERED]"],-["reset_password_token",-"[FILTERED]"],-["reset_password_sent_at",-"[FILTERED]"],-["remember_created_at",-nil],-["failed_attempts",-0],-["unlock_token",-"[FILTERED]"],-["locked_at",-nil],-["created_at",-"2024-03-20-18:59:15.403932"],-["updated_at",-"2024-03-20-18:59:15.403932")
TRANSACTION (55.9ms) commit transaction
=> true
The user will now be able to log in normally
Deleting a user manually
This can also be done through the rails console. You need to be certain you're able to do an activerecord query that returns the correct user! For example (in console):
firstuser = User.where(id:1)
User.delete(firstuser)
The user now cannot log in.
The sqlite database however will still be able to grep a match for the old deleted user, at least in this example using sqlite. Understand that deleting an old user can still leak references to it for anyone who has access to the database (e.g. if published as part of a GitHub repo) This can be uncovered using grep
(for known user email) or strings
(to expose user emails)
It's difficult to allow the user to edit their own password when logged in if not using registerable
If you're not using :registerable, then it's not clear (to me) how to allow users to edit their own password from inside the system without using a password reset token through email.
There are some instructions in the official devise Wiki on this at https://github.com/heartcombo/devise/wiki/How-To:-Allow-users-to-edit-their-password Unfortunately these instructions seem out of date, or at least don't work with what I'm doing here, or else I'm doing something wrong. In any case, it's not an easy change to make.
The reason it's not easy is that info from here https://www.ruby-forum.com/t/update-password-with-devise/198201/6 shows that by default changing passwords is done via the reset link. Therefore, the route /users/password/edit
doesn't go anywhere. This edit password feature is part of the recoverable module. You can access this with Registerable, but if you don't have registerable on, you can't change password easily (without having to code own stuff like controller for it, from looks of it).
You manually update a user password in the rails console as follows from https://stackoverflow.com/questions/25539545/how-do-i-manually-change-update-a-user-password-in-devise-3-2-x
user = User.find_by_email("myemail.com")
user.password = "newSuperSecret"
user.save
However, it feels like a bit of a catch-22 that users can only update own password with registerable module, BUT on other hand if you don't want random ppl signing up then you should disable registerable.... which means users can't update own password from when already inside the app, other than using the reset mechanism via email, which you (for security reasons) might not want to have???
User model name(s) is mount point(s) - can't change easily or at all
If using User model, the sign up, sign in, etc URL's will be under /users. Admin model will be under /admins. Think carefully what model name you want before installation. Note to self: be sure to have noindex etc on those pages (and/or on .htaccess??)
Can you change mount point later? (Don't know - looks like it wouldn't be easy since it is based on the name of the model produced) If I could periodically move it easily without repercussions (i.e. without errors or database issues) this could be useful for a 1-person system.
You can have as many different models as you want in your app, for example if implementing scoped user roles. You can't allow their sign in URL's to collide though, so you'd have to be clear what you're mapping to what if you're making any changes to routes.rb
Consider how to mitigate against malicious time-outs
Once someone's account email is known to a bad actor, they may choose to maliciously time out someone. This is why some other unlock method may be good other than just time (e.g. via email) - which can easily be done with Devise, but this is not in the scope of this brief overview
In addition to lockable, use another layer of protection against brute force
Lockable shouldn't be the only layer of defense against a brute force attack, but it's probably the best first option. Also consider one or both of rack-attack
and the Apache module mod-evasive
Neither of these will protect against a distributed attack though.
See see https://www.rubysos.com/std-lib/how-to-test-my-ruby-on-rails-website-against-brute-force-properly/
Further things to look into
- confirmable
- trackable
- omniauthable
Further reading
See https://interviewprep.org/devise-ruby-on-rails-interview-questions/ for more info about considerations when using devise