Subscribe to access all episodes. View plans →

#42: User Authentication with Phoenix

Published April 18, 2018

Elixir 1.5

Phoenix 1.3

Comeonin 4.0

bcrypt_elixir 1.0

View Source on GitHub


There are a lot of different packages to help with authentication in Elixir applications.

While we’ll be covering a them in future episodes, I wanted to start by showing an example of how we could write our own user authentication - from scratch.

I hope this will provide an introduction to authentication that you can build and improve on when implementing it in an Elixir application of your own.

Alright - Let’s get started by taking a look at the application we’ll be using. Here we have our movie app and right now anyone can can create and edit movies. What we want to do is require a user to be signed in before they can create or edit a movie.

Let’s get started by running the phoenix html generator to bootstrap our user registrations and we’ll create a context called “Accounts”, “User” for our User module, which will be our Ecto schema module, “users” which is the plural of our User module - this is used as our table name in the database.

And let’s keep this simple and just give our users two fields - a username that will be a unique string. And a password field that we’ll call encrypted_password. We’ll talk about why we chose to call it that in a bit.

For now let’s run our generator.

$ mix phx.gen.html Accounts User users username:string:unique encrypted_password:string
* creating lib/teacher_web/controllers/user_controller.ex
* creating lib/teacher_web/templates/user/edit.html.eex
* creating lib/teacher_web/templates/user/form.html.eex
* creating lib/teacher_web/templates/user/index.html.eex
* creating lib/teacher_web/templates/user/new.html.eex
* creating lib/teacher_web/templates/user/show.html.eex
* creating lib/teacher_web/views/user_view.ex
* creating test/teacher_web/controllers/user_controller_test.exs
* creating lib/teacher/accounts/user.ex
* creating priv/repo/migrations/{timestamp}_create_users.exs
* creating lib/teacher/accounts/accounts.ex
* injecting lib/teacher/accounts/accounts.ex
* creating test/teacher/accounts/accounts_test.exs
* injecting test/teacher/accounts/accounts_test.exs

Add the resource to your browser scope in lib/teacher_web/router.ex:

resources "/users", UserController

Remember to update your repository by running migrations:

$ mix ecto.migrate

Now let’s open router.

Our sign up is going to be limited to creating a user. So let’s add only add the new and create actions and because this is for users to register, let’s change the url “registrations”.

lib/teacher_web/router.ex

...
scope "/", TeacherWeb do
  pipe_through :browser # Use the default browser stack
  get "/", MovieController, :index
  resources "/movies", MovieController
  resources "/registrations", UserController, only: [:create, :new]
end
...



Now that we’ve added our route let’s run our migrations.

$ mix ecto.migrate

And since we’re limiting our routes, let’s open our user_controller.ex and we’ll remove all actions, except for new and create.

Then once a user has registered for the site, let’s direct them to the movie index path and let’s update the message we display as well.

lib/teacher_web/controllers/user_controller.ex

...
def create(conn, %{"user" => user_params}) do
  case Accounts.create_user(user_params) do
  {:ok, user} ->
    conn
    |> put_flash(:info, "Signed up successfully.")
    |> redirect(to: movie_path(conn, :index))
  {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "new.html", changeset: changeset)
  end
end
...

Then we can remove the templates we won’t need: edit.html.eex, index.html.eex, and show.html.eex. Then let’s open the new.html.eex template and let’s remove the “back” link since we don’t have that page and let’s change the text to read “Sign up”.

Template path: lib/teacher_web/templates/user/new.html.eex

<h2>Sign up</h2>
<%= render "form.html", Map.put(assigns, :action, user_path(@conn, :create)) %>

Then we’ll open form.html.eex we’ll update the label to make it a little more user-friendly and just have it read password. Let’s also mask our password input by changing it to password_input.

Template path: lib/teacher_web/templates/user/form.html.eex

...
<div class="form-group">
<%= label f, :password, class: "control-label" %>
<%= password_input f, :encrypted_password, class: "form-control" %>
<%= error_tag f, :encrypted_password %>
...

Let’s also open our app.html.eex and include our Sign up link.

Template path: lib/teacher_web/templates/layout/app.html.eex

<%= link "Sign up", to: user_path(@conn, :new) %>

Now let’s go back to the command line and start our server.

$ mix phx.server

And let’s check out our new page at: http://localhost:4000/registrations/new

And great - our sign up page is working.

Now if we submitted the form here we would create a user, however we don’t want to use the current implementation because we’re not encrypting the password before we save it.

Let’s use the comeonin library to hash our passwords.

Let’s open our mixfile

And we’ll add comeonin to our dependencies.

Then we need to pick a password hashing algorithm. The comeonin docs list three different ones to choose from. Let’s use bcrypt.

mix.exs

...
  {:comeonin, "~> 4.0"},
  {:bcrypt_elixir, "~> 1.0"},
...

We’ll go to the command line and download our dependencies.

$ mix deps.get

Great now let’s go back our user.ex module and let’s alias Comeonin.Bcrypt.

Now we need to hash our password before we save it. Let’s go down to our changesetand we’ll add the update_change function. This will invoke our function Bcrypt.hashpwsalt if there is a change for the encrypted_password key and update it with the return of that function.

lib/teacher/accounts/user.ex

defmodule Teacher.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Teacher.Accounts.User
  alias Comeonin.Bcrypt

  schema "users" do
    field :encrypted_password, :string
    field :username, :string
    timestamps()
  end

  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:username, :encrypted_password])
    |> unique_constraint(:username)
    |> validate_required([:username, :encrypted_password])
    |> update_change(:encrypted_password, &Bcrypt.hashpwsalt/1)
  end
end

Now we can sign up for the site and the submitted password will be encrypted before being saved to our database. Let’s also set the session once a user has registered.

We’ll open our user_controller.ex and after our user has been created, let’s put our user id on the session as :current_user_id.

lib/teacher_web/controllers/user_controller.ex

...
{:ok, user} ->
  conn
  |> put_session(:current_user_id, user.id)
  |> put_flash(:info, "Signed up successfully.")
  |> redirect(to: movie_path(conn, :index))
...

With that let’s start the server.

$ mix phx.server

Now let’s go to our browser http://localhost:4000/registrations/new and create our user and great it redirects us to the homepage and gives us our message. If we check out database, we see our user was created.

Now that we can sign up, we need a way to check if a user has a session - and if they do - a way to sign out. There are a few different ways we could do this, but what we’ll do here is create a new directory named helpers in teacher_web. Then we can create a new module for our helper functions grouped by their functionality.

So here we’ll create a new file auth.ex that will house our helper functions we need for authentication. Then we can define our module.

And let’s create a function to get our current_user_id from the session.

lib/teacher_web/helpers/auth.ex

defmodule TeacherWeb.Helpers.Auth do

  def signed_in?(conn) do
    user_id = Plug.Conn.get_session(conn, :current_user_id)
    if user_id, do: !!Teacher.Repo.get(Teacher.Accounts.User, user_id)
  end

end

Since checking whether a user has a session is pretty common and we’ll probably want to use this function across many different views let’s configure it so it’s available to our different views.

Let’s open our teacher_web.ex module and we’ll include our auth.ex module in our view. Let’s also specify the functions we want to make available with only: [signed_in?: 1].

Great, now we can use our signed_in? helper function across our different views.

lib/teacher_web.ex

...
def view do
  quote do
    ...
    import TeacherWeb.Helpers.Auth, only: [signed_in?: 1]
  end
end
...

With that let’s open our app.html.eex template and we’ll add our new helper function to display our link to sign out if the user is signed in. And if there’s no session we’ll display a link to sign in or up.

Template path: lib/teacher_web/templates/layout/app.html.eex

...

<%= if signed_in?(@conn) do %>
  <%= link "Sign out", to: "" %>
<% else %>
  <%= link "Sign In", to: "" %>
  |
  <%= link "Sign up", to: user_path(@conn, :new) %>
<% end %>
...

Then if we go to the browser, we see that our helper function is working and we see a sign out link.

Now we need to build a way for users to sign in and sign out. Let’s start by defining our routes. We’ll open our router.ex module.

Then let’s add three new routes. We’ll create one for our sign-in form, one that we’ll post to when the sign-in form is submitted, and then one to sign-out.

lib/teacher_web/router.ex

...
scope "/", TeacherWeb do
  ...
  get "/sign-in", SessionController, :new
  post "/sign-in", SessionController, :create
  delete "/sign-out", SessionController, :delete
end
...

Then let’s create a controller to handle our new requests. We’ll call our new controller session_controller.ex. Then let’s define the three actions we’ll need: new, create, and delete.

Our new action will just render our new.html template. In our create action we’ll receive the sign-in form submission, lookup the user, and if the submitted password matches the user’s password, we’ll sign them in. Otherwise, we’ll send them back to the sign in page with a message.

To start we need a way to look the user up by their username. Since we have an accounts context, let’s use that. We’ll open our accounts.ex module. Then let’s create a new function get_by_username and in it we’ll just get our user by their username. Since username can be nil let’s set another function clause for when the username is nil and in that case we’ll just return nil.

Lib/teacher/accounts/accounts.ex

...
def get_by_username(username) when is_nil(username) do
  nil
end
def get_by_username(username) do
  Repo.get_by(User, username: username)
end
...

Then let’s update our function to match on our session params.

We’ll group our session params under the key "session", so we can pattern match on that here. We’ll assign them to a variable auth_params. Then the first thing we’ll want to do is get our user with our new function passing in the username from our auth_params.

Now we need to check the user’s password with what was submitted.

If we look at the docs for comeonin and look at the check_pass) function we can pass it our user struct and the submitted password.

It requires there to be a key of either :password_hash or :encrypted_password when doing the comparison and since ours is :encrypted_password this will work.

What’s nice about this is that if a user is not found and we pass in nil a dummy verification is run to, which according to the docs make user enumeration, using timing information, more difficult.

Back in our session_controller we’ll use a case statement with the check_pass function. Let’s first match against the success case with an :ok tuple. And if a user authenticated we’ll set the session, put a flash message to let them know they are signed in.

Then let’s redirect them to the homepage. Now let’s match against an error - when a submitted password doesn’t match what we have for the user.

First let’s return a flash message letting them know there was an issue with their username or password. And then let’s just render the sign on page again.

Now let’s finish our delete function, which will be triggered when a user clicks our “Sign out” link. We’ll take the connection and pipe it into the delete_session function. To delete user’s session. Then let’s add a flash message to let them know they’ve signed out. Finally we’ll redirect them to our movies page.

lib/teacher_web/controllers/session_controller.ex

defmodule TeacherWeb.SessionController do
  use TeacherWeb, :controller

  alias Teacher.Accounts

  def new(conn, _params) do
    render(conn, "new.html")
  end

  def create(conn, %{"session" => auth_params}) do
    user = Accounts.get_by_username(auth_params["username"])
    case Comeonin.Bcrypt.check_pass(user, auth_params["password"]) do
    {:ok, user} ->
      conn
      |> put_session(:current_user_id, user.id)
      |> put_flash(:info, "Signed in successfully.")
      |> redirect(to: movie_path(conn, :index))
    {:error, _} ->
      conn
      |> put_flash(:error, "There was a problem with your username/password")
      |> render("new.html")
    end
  end

  def delete(conn, _params) do
    conn
    |> delete_session(:current_user_id)
    |> put_flash(:info, "Signed out successfully.")
    |> redirect(to: movie_path(conn, :index))
  end
end

Now we need to create our sign in page. We’ll first create a new view session_view.ex.

lib/teacher_web/views/session_view.ex

defmodule TeacherWeb.SessionView do
  use TeacherWeb, :view

end

Then let’s create our template. we’ll create a new directory, templates/session. And we’ll create a new template new.html.eex. I’ll paste in our sign in form here, but let’s walk through it.

First we’re creating our form. And instead of using a changeset, we’re passing in our connection. We’re telling the form to send our data to the create action in our session_controller.ex

And then [as: :session] sends our form data here in under key session in our params.

Then we’ve just got inputs for the username, password, and a “Sign in” button.

Template path: lib/teacher_web/templates/session/new.html.eex

<h1>Sign in</h1>
<%= form_for @conn, session_path(@conn, :new), [as: :session], fn f -> %>
  <%= text_input f, :username, placeholder: "username" %>
  <%= password_input f, :password, placeholder: "password" %>
  <%= submit "Sign in" %>
<% end %>

Now let’s open our app.html.eex and update our sign in and out links with our new routes.

Template path: lib/teacher_web/templates/layout/app.html.eex

<%= if signed_in?(@conn) do %>
  <%= link "Sign out", to: session_path(@conn, :delete), method: :delete %>
<% else %>
  <%= link "Sign In", to: session_path(@conn, :new) %>
  |
  <%= link "Sign up", to: user_path(@conn, :new) %>
<% end %>

Now we can test it out. If we submit an incorrect username/password combo, we get our expected message. And then if we sign in with the correct username/password - great it works.

Now that we can register and sign in, we are in a place to implement our original functionality - limiting movie create and edit access to signed in users. Let’s go back to our movie_controller.ex

We’ll need to create a function that checks if the user has a session. If it exists we’ll allow the action to proceed. If not, let’s display a message and redirect to the homepage.

Let’s do this by creating a function plug that we’ll call on specific actions. First we’ll need to create our function plug. If you aren’t sure how function plugs work then check out episode 19 where I do an introduction to them.

Ours will be a private function called check_auth. Now inside the function we’ll use the get_session function to check if our current_user_id exists.

If it does let’s get our current user with Accounts. get_user! then let’s assign our user to the connection. this way we can access the user struct from the connection without having to do multiple calls to the database each time we need to get the user.

We’ll also need to alias our Accounts context.

If not, let’s display a flash message letting the user know they need to be signed in. Then let’s redirect them to our movie’s index page.

And call halt to prevent any other plugs from being invoked.

With our function defined, let’s call the plug conditionally, when the action is new, create, edit, update, or delete.

lib/teacher_web/controllers/movie_controller.ex

…
alias Teacher.Accounts

plug :check_auth when action in [:new, :create, :edit, :update, :delete]

defp check_auth(conn, _args) do
  if user_id = get_session(conn, :current_user_id) do
  current_user = Accounts.get_user!(user_id)

  conn
    |> assign(:current_user, current_user)
  else
    conn
    |> put_flash(:error, "You need to be signed in to access that page.")
    |> redirect(to: movie_path(conn, :index))
    |> halt()
  end
end
…

Let’s go to the browser and test it out.

And trying to create a movie or edit one gives us our error message. Our movie app is working as we expect it. Requiring users to authenticate before create or editing movies.

© 2024 HEXMONSTER LLC