Persisting passwords in plain text does not seem to be a very good idea. If for any reason an attacker manages to get access to your database they would be able to see all your users’ passwords without any restriction, giving them the possibility to authenticate as your users… and as you know, despite of constantly advising not to do so, still many people use the same password in many sites… so it can even get worse… (I hope it is not your case 😉). Fortunately Rails gives you a way to easily store passwords in an irreversible encrypted way, as well as auxiliary authentication and validation methods.
Adding has_secure_password
Let us see what you need to do to use has_secure_password
in your Rails application.
The macro has_secure_password
adds methods to set and authenticate against a BCrypt password. This indicates that you need to include a library that provides BCrypt logic to your Rails application. Since this is a common use case, the default Gemfile
generated by your Rails application already includes the bcrypt
gem. The only thing you need to do is uncomment that line, as you can see in the following image, and then run bundle install
.
Now run bundle install
to get this dependency installed into your Rails app.
1 |
$ bundle install |
has_secure_password
is defined in ActiveModel::SecurePassword
. If you are using has_secure_password
with Active Record, as I will be doing in the examples bellow, you do not need to do anything extra since Active Record automatically includes ActiveModel::SecurePassword
.
To see has_secure_password
in action, create a User
model with some fields, in this example just a unique email
, and a password_digest
field. It is important that the field in which the password is going to be persisted ends with _digest
. In this example, it will be the default password_digest
.
Let’s take advantage of Rails’ scaffolding to create this User
model. In a terminal, execute the following:
1 |
$ bin/rails g scaffold User email:uniq password:digest |
Once the scaffold generator has created all the needed files, it is time to migrate the database. In a terminal, run the following command:
1 |
$ bin/rails db:migrate |
The resulting database schema should look similar to the one in the following listing:
1 2 3 4 5 6 7 8 9 |
ActiveRecord::Schema[7.0].define(version: 2022_07_15_150105) do create_table "users", force: :cascade do |t| t.string "email" t.string "password_digest" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["email"], name: "index_users_on_email", unique: true end end |
As you can see, Rails has added a password_digest
field, which as we mentioned before, has the _digest
suffix, necessary for has_secure_password
to work.
If the field that is going to be used to persist the passwords is called password_digest
, then in the User
model you just need to add the has_secure_password
macro with no further indications.
1 2 3 |
class User < ApplicationRecord has_secure_password end |
If instead you decided to name it for example recovery_password_digest
, then in the User
model you need to indicate the name of the field, without the _digest
suffix. Like this:
1 2 3 |
class User < ApplicationRecord has_secure_password :recovery_password end |
If you start the Rails server by running bin/rails server
on a terminal, and you navigate to the user creation page, you should see something similar to the following image.
Since sign up and authentication forms tend to be different in each Rails application we will now focus on the methods and validations added by has_secure_password
. To do that we will open a Rails console and run some commands there. To start the Rails console, in a terminal, run bin/rails c
, like this:
1 2 3 |
$ bin/rails c Loading development environment (Rails 7.0.3.1) irb(main):001:0> |
Now let’s create a user, first without providing a password, so that has_secure_password
validations kick in telling you that you need to provide one, and then providing a password:
1 2 3 4 5 6 7 |
(1.0ms) SELECT sqlite_version(*) => #<User:0x0000000106653400 id: nil, email: "[email protected]", password_digest: nil, created_at: nil, updated_at: nil> irb(main):002:0> user.save => false irb(main):003:0> user.errors => #<ActiveModel::Errors [#<ActiveModel::Error attribute=password, type=blank, options={}>]> |
Let’s now provide a password so that the user can be persisted:
1 2 3 4 5 6 7 |
irb(main):004:0> user.password = "super_secret" => "super_secret" irb(main):005:0> user.save TRANSACTION (0.1ms) begin transaction User Create (2.7ms) INSERT INTO "users" ("email", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["email", "[email protected]"], ["password_digest", "[FILTERED]"], ["created_at", "2022-07-15 16:06:55.982187"], ["updated_at", "2022-07-15 16:06:55.982187"]] TRANSACTION (1.5ms) commit transaction => true |
If you pay attention to that transaction log, you will notice that Rails is not persisting anything in a password
field, but instead in password_digest
, whose value appears already FILTERED
in the logs for security reasons without having to do anything about it. AWESOME 🙌.
If you wanted to see what is in there, you can just fetch the user’s password_digest
:
1 2 |
irb(main):006:0> user.password_digest => "$2a$12$phEpOAescgItyLOeCrCXke.L6yceYRAGih6L7pqQNfrqm2VhIiGTi" |
As you can see, it has been encrypted so that it does not have anything to do with the original value. This encryption is irreversible, which means, that the original value cannot be decrypted from the value of password_digest
.
In order to authenticate the user, has_secure_password
provides the authenticate
method, which accepts a password string as a parameter, encrypts it using the same algorithm as the one used to persist password_digest
and then compares the result with the value stored at password_digest
. If they match, the result will be true
, otherwise false
.
In a typical Rails application’s SessionsController, you would first fetch the user by email and then check using the authenticate
method like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]] => #<User:0x00000001060e82e0 ... irb(main):008:0> user.authenticate("wrong_password") => false irb(main):009:0> user.authenticate("super_secret") => #<User:0x00000001060e82e0 id: 1, password_digest: "[FILTERED]", created_at: Fri, 15 Jul 2022 16:06:55.982187000 UTC +00:00, updated_at: Fri, 15 Jul 2022 16:06:55.982187000 UTC +00:00> |
There is an extra validation that it is ignored by Rails unless you provide a value for it: password_confirmation
. When a password_confirmation
is provided, both password
and password_confirmation
values must match so that the user can be persisted:
1 2 3 4 5 6 7 8 9 10 11 |
irb(main):010:0> user = User.new(email: "[email protected]", password: "very_secret", password_confirmation: "notmatch") => #<User:0x0000000106cccac0 id: nil, email: "[email protected]", password_digest: "[FILTERED]", created_at: nil, updated_at: nil> irb(main):011:0> user.save => false irb(main):012:0> user.password_confirmation = "very_secret" => "very_secret" irb(main):013:0> user.save TRANSACTION (0.1ms) begin transaction User Create (0.9ms) INSERT INTO "users" ("email", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["email", "[email protected]"], ["password_digest", "[FILTERED]"], ["created_at", "2022-07-15 16:26:44.819629"], ["updated_at", "2022-07-15 16:26:44.819629"]] TRANSACTION (1.4ms) commit transaction => true |
If you just want to ignore all these validations you can just provide the option validations: false
to your has_secure_password
macro in the User model. Like this:
1 2 3 |
class User < ApplicationRecord has_secure_password validations: false end |
And that is basically it! Now that you know how to secure passwords, in the next post we will take a look the recently added by Rails 7 Active Record Encryption feature, and the differences it has with has_secure_password
.
If you enjoyed this post, do not forget to subscribe so that you do not miss any of my future updates. If you want me to write about something in particular, you can let me know about it in the comment section below.
See you in my next post! Adiós!
One Response