Subscribe to access all episodes. View plans →
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 changeset
and 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.
Chris O'Halloran
6 years agoFollowing along, but I am getting this error on sign-in (sign-up and sign-out function as expected)
Phoenix.ActionClauseError at POST /sign-in could not find a matching MovieWeb.SessionController.create clause to process request. This typically happens when there is a parameter mismatch but may also happen when any of the other action arguments do not match. The request parameters are:
Alekx
6 years agoHey Chris, what does the form in your session’s
new.html.eex
template look like along with thecreate
func on yousession_controller.ex
?Bill Mitchell
6 years agoGreat video! At the start you stated there are many packages available to help with authentication and you explore them in future videos? Do you know if those videos have been created yet? If not, do you mind listing what other packages you’d suggest taking a look at? Thanks!
Alekx
6 years agoHey Bill, I’ve got a few finished and a few in the pipeline. I’ve done one using Ueberauth as well as passwordless auth with Veil. Guardian is another package you may want to look at as well as Coherence. Hope this helps!
Bill Mitchell
6 years agoThanks Alekx!
zairecrypto
4 years agoHello Alekx,
Followed along, but I am getting no route found for GET /sign-out.
Here is the link to my project in case you want to check further https://github.com/zairecrypto/test.git
Thank you
Alekx
4 years agoDouble check that the route you’re using is a
DELETE
and not aGET
. Thanks for providing a repo, is that happening when you click your link here: https://github.com/zairecrypto/test/blob/master/lib/my_tests_web/templates/layout/app.html.eex#L24