Managing Registration Changesets

You’ve already seen a changeset for creating a new user, the one that handles the name and username. Let’s review that now:

 def​ changeset(user, attrs) ​do
  user
  |> cast(attrs, [​:name​, ​:username​])
  |> validate_required([​:name​, ​:username​])
  |> validate_length(​:username​, ​min:​ 1, ​max:​ 20)
 end

The Ecto.Changeset.cast function converts a raw map of user input to a changeset, accepting only the :name and :username keys. Then, we fire a validation limiting the length of valid usernames to twenty characters. A failing validation places errors in the changeset so we can display them to the user.

As you might expect, you’ll use one changeset per use case. Our existing changeset handles all the attributes except passwords. We can safely use it for updating nonsensitive information such as a form on a profile page. We’ll build a separate changeset to manage more sensitive data such as credential changes.

For the password changeset, we’ll add two new fields, :password and :password_hash. The :password field will contain the password in plain text, but for security reasons we won’t store that field in the database. Instead, we will hash the password in the :password_hash field we added to the users table way back in Chapter 4, Ecto and Changesets. Now, we’ll define those two fields in the schema:

 field ​:password​, ​:string​, ​virtual:​ true
 field ​:password_hash​, ​:string

We marked the :password field as virtual: true. Virtual schema fields in Ecto exist only in the struct, not the database.

Now let’s create our separate changeset to handle user registrations:

 def​ registration_changeset(user, params) ​do
  user
  |> changeset(params)
  |> cast(params, [​:password​])
  |> validate_required([​:password​])
  |> validate_length(​:password​, ​min:​ 6, ​max:​ 100)
  |> put_pass_hash()
 end

There’s not much to see here. We defined a registration_changeset function which creates a new changeset, casts the :password field and validates it. Then, our function delegates to the put_pass_hash function to compute and store the user hash in the database, like this:

 defp​ put_pass_hash(changeset) ​do
 case​ changeset ​do
  %Ecto.Changeset{​valid?:​ true, ​changes:​ %{​password:​ pass}} ->
  put_change(changeset, ​:password_hash​, Pbkdf2.hash_pwd_salt(pass))
 
  _ ->
  changeset
 end
 end

We check to see if the changeset is valid so we won’t waste time hashing an invalid or missing password. Then, we use comeonin to hash our password, following the instructions in its readme file. Finally, we put the result into the changeset as password_hash. If the changeset is invalid, we simply return it to the caller.

These password rules are light

images/aside-icons/warning.png

We’re creating an intentionally lax password so readers can focus on learning concepts instead of memorizing passwords. You will want to use more strict password requirements in a production system.

Here you can see how easy it is to compose with changesets. We used our base User.changeset function to cast and validate the name and username parameters. Then we validated our virtual password field inside our registration changeset. Notice that it’s trivial to validate our virtual password field, though we’re not actually storing that value in the database! Persistence is not strongly coupled to our change policies.

Keep in mind that this is an example application, and you should configure your own password rules to fit your scenario. If you would like, OWASP[17] has an excellent set of guidelines you can follow depending on your specific requirements.

Let’s take it for a spin.

Open up a console and follow along. If you’ve been following along and aren’t working on a new console, you can safely skip alias RumblWeb.Router.Helpers, as: Routes.

Let’s try out our changeset:

 iex>​ alias Rumbl.Accounts.User
 iex>​ alias RumblWeb.Router.Helpers, ​as:​ Routes
 iex>​ changeset = User.registration_changeset(%User{}, %{
 ...>​ ​username:​ ​"​​max"​, ​name:​ ​"​​Max"​, ​password:​ ​"​​123"
 ...>​ })
 #Ecto.Changeset<​...>
 iex>​ changeset.valid?
 false
 
 iex>​ changeset.changes
 %{
  name: "Max",
  username: "max",
  password: "123"
 }

As we expected, creating a user with our registration changeset and a bad password results in an invalid changeset. When we inspect the changeset.changes, we can see that password_hash is missing because we didn’t bother hashing a password we knew to be invalid.

Let’s continue and see what happens when we create a valid registration changeset:

 iex>​ changeset = User.registration_changeset(%User{}, %{
 ...>​ ​username:​ ​"​​max"​, ​name:​ ​"​​Max"​, ​password:​ ​"​​asecret"
 ...>​ })
 #Ecto.Changeset<
  action: nil,
  changes: %{
  name: "Max",
  username: "max",
  password: "asecret",
  password_hash:
  "$pbkdf2-sha512$r7zRM4aQgSUGlOy4483cFe1UouMC/9emcOI75MhgDQ6A9WNWBpfr."
  },
  errors: [],
  data: #Rumbl.Accounts.User<>,
  valid?: true
 >

Check to see if it’s valid, and see the changes:

 iex>​ changeset.valid?
 true
 
 iex>​ changeset.changes
 %{
  name: "Max",
  username: "max",
  password: "asecret",
  password_hash:
  "$pbkdf2-sha512$r7zRM4aQgSUGlOy4483cFe1UouMC/9emcOI75MhgDQ6A9WNWBpfr."
 }

When given a valid username and password, our changeset applies the put_pass_hash function and puts a change for our password_hash field, but we now have an issue. The users we inserted up to this point lack account passwords, which won’t be valid with the system’s new behavior where we expect all accounts to have one. Let’s fix that now by updating our existing users with properly hashed temporary passwords. Key this into your IEx session. If you’re using an existing window, you may need to recompile with recompile:

 iex>​ recompile()
 iex>​ alias Rumbl.Repo
 iex>​ for u <- Repo.all(User) ​do
 ...>​ Repo.update!(User.registration_changeset(u, %{​password:​ ​"​​temppass"​}))
 ...>​ ​end

Now our new and existing users alike will have valid, secure passwords.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.42.243