In this lesson, we're going to use two great Elixir libraries, Comeonin and Guardian, to make sure that the users of our app can sign up, log in and maintain a session.
Hashing passwords with Comeonin
The first thing we need to do is continue where we left off in the previous lesson. We want to turn the user's password
param into an encrypted_password
that we can store safely in our database.
To accomplish this task, we're going to use the Comeonin library, so you'll also learn how to use external Elixir dependencies in your app. Open the file mix.exs
at the root of your project, scroll all the way down to the private deps
function, and add a row for comeonin
right before the list closes:
Now that that's there, we can run this Mix task to install the newly added dependency, and recompile our app (you might also want to restart your Phoenix server just in case).
Success! After the installation, we can update our create_user/1
-function in the Accounts
module to take advantage of the Comeonin library. We want to extract the user's password from the parameters that are sent in, encrypt it, and then put it in the changeset using Ecto.Changeset
's put_change
:
While we're in this file, we're also going to add some more constraints to the changeset function so that we can give clearer error messages to the user when something is wrong:
These functions should be quite easy to understand even if they're new. unique_constraint
ensures that there are no duplicates of a particular field (since we can't have 2 users with the same username for instance) while validate_format
makes sure that a field matches a certain regex (for the email, we just check if there's an @
-sign). validate_length
in combination with min: 4
makes sure that the field is at least 4 characters long.
Now, we can finally create our first real user! If you want, you can delete the user that we created previously in the database, so that you can get a fresh start! I'm going to type in tristan
as my username, tristan@ludu.co
as my email, andpassword
as my password. Feel free to change these values.
Updating the login page
So we've completed the signup part. Now we also need to let the user log in, and stay logged in.
We'll start with the easy part of just creating the necessary routes, templates and controller functions. You should be pretty familiar with this process now, since it's very similar to the one we had for the signup functionality.
Let's tweak our login template so that it has a dynamic form that's similar to the one on our signup page. The difference is that we won't use a changeset for this one, so we need to specify the key under which all the input values are going to be sent under (in this case, we'll choose :credentials
):
Based on this, you should see what our next steps should be. We need to create the login_user/2
-function in our PageController. For now, login_user/2
won't do anything except show an error flash if the user tries to sign in.
And finally, no route is complete without an entry in the router.ex
-file! Let's specify the path as POST /login
, and add it right under POST /signup
:
Creating a Session
So how do we check if the given username and password is correct? Well, the Comeonin library has a handy checkpw/2
-function that helps us compare an encrypted password with a plain text password! Easy peasy!
Before implementing this though, we should take a step back and reflect about how many steps are involved in this login process. It seems like we would have to perform at least the following steps:
Get the user's form parameters
Read the username and find the matching user from the database
Compare the matching user's encrypted password to the supplied password
If it matches, create a session so that the logged-in user persists
Hm, that seems like quite a lot of functionality! Let's create a new Session
module in our "accounts" context to handle some of this, so that we have some clear separation of concerns. This is how we envision it to work in our controller afterwards:
Let's start coding that new module! The first thing we'll do is alias the stuff we know we'll need and create an empty authenticate/1
-function:
We now want to get the username and password from the credentials and fetch the user from the database that matches that username. This can easily be done with some clever pattern matching and one line of code:
Now that we have the user, we want to compare this user's encrypted_password
to the given_password
. For that we'll create a check_password/2
-function:
Note that we've defined the function check_password/2
twice.
The first one is called in case user
turns out to be nil
(Repo.get_by
will return nil
if it can't find a user that matches the given username).
The second one uses pattern matching to get the encrypted_password
from the user struct. We then compare that password to the one given by the user thanks to Comeonin's checkpw/2
function.
In both cases, we either return a tuple with the :ok
atom and the user struct, or a tuple with :error
and an error message.
This is good enough for now! Let's go back to our Page Controller and use this module. This basically mirrors the functionality of create_user
– we always render the login.html
template, but based on the return value of Session.authenticate
, we either add an info flash or an error flash:
Nothing spooky here, just some classic pattern matching. You should now be able to see the different messages when you play around with the login page!
Persisting the user with Guardian
Even though our flash message says "Logged in as tristan", we haven't actually written any code to persist the user yet. All we've done so far is verify that the login credentials were valid, but there's nothing keeping us logged in yet. Thankfully, the rest is pretty easy thanks to the awesome Guardian library.
Let's add Guardian as a dependency in our mix.exs
file, like we did with Comeonin. Until Guardian is updated (see issue), we also need to add override:true
on the phoenix
dependency:
If we follow Guardian's installation instructions, you'll also see that we need to set some configuration options. We can do this at the very bottom of config/config.exs
:
Please note that our current secret_key
("5ecret_k3y") is terribly insecure. In order to generate one that's actually secure for production usage, you can use the Mix task mix guardian.gen.secret
.
As you can also see from the code, our configuration requires that we have a module called Messengyr.Auth.Guardian
. We'll just copy the one that Guardian provides in their documentation and create a file for it in our "accounts"-context:
Alright, stop your server and run mix deps.get
to install and recompile. Now you can run mix phx.server
again, and we're ready to use Guardian in our app!
Saving the logged-in user to the current session is as easy as adding a line to the login_user/2
-function in our PageController! We simply use Guardian's special sign_in/2
function like this:
If we log in again now, our user will be saved to the session (although you won't see it)! The final step is to fetch the info from this session on every request so that we can display the user, because there's no point in logging in if everything still looks the same right?
Fetching the session info
To tell Phoenix that we want to fetch the session info on every HTTP request, we need to go back to the Router file and create a special pipeline.
We'll call this pipeline browser_session
, and all it does is use two Guardian plugs – VerifySession (which checks if we're logged in), and LoadResource (which checks who exactly is logged in).
Following Guardian's documentation, we create a special pipeline module:
Then we use it in a brand new browser_session
pipeline which we put underneath the existing browser
pipeline:
And then we make sure that we use this pipeline in our default scope:
This should be enough to make it work! Let's test it out by experimenting with the landing page. We'll fetch the user with Guardian's current_resource
function and log it in the index
function of the Page Controller.
Reload the landing page (/
) in your browser and you should see this in your console, provided that you logged in before:
We now have evidence that our signup and authentication works. The next step is to improve the UI of our Phoenix app to take it from being merely a website, to a full-blown web app!
Comments
You need a Ludu account in order to ask the instructor a question or post a comment.
I am getting the below error with your code.
[error] #PID<0.551.0> running MessengyrWeb.Endpoint (connection #PID<0.543.0>, stream id 4) terminated
Server: localhost:4000 (http)
Request: POST /login
** (exit) an exception was raised:
** (ArgumentError) Comeonin.Bcrypt.checkpw has been removed.
Add {:bcrypt_elixir, "~> 2.0"} to the deps in your mix.exs file,
and use Bcrypt.verify_pass instead.
So i tried below code but still getting issues with it.
Any ideas?
WHat is message in this function?
def login_user(conn, %{"credentials" => credentials}) do
case Session.authenticate(credentials) do
{:ok, %{username: username}} ->
conn
|> put_flash(:info, "Logged in as #{username}!")
|> render("login.html")
{:error, message} ->
conn
|> put_flash(:error, message)
|> render("login.html")
end
end
message
is the error message that gets thrown.ok but is it supposed to as a variable? Because this is the error i am getting.
warning: variable "message" does not exist and is being expanded to "message()", please use parentheses to remove the ambiguity or change the variable name
lib/messengyr_web/controllers/page_controller.ex:53: MessengyrWeb.PageController.login_user/2
mix phoenix.gen.secret is deprecated. Use phx.gen.secret instead.
Or
guardian.gen.secret
@5avage: Thanks for the heads up. I've updated the lesson!
I also needed to include {bcrypt_elixir, "~> 1.0"}