Implementing Login and Logout

We made great progress in the last section. We created a module plug that loads information from the session, used this information to restrict user access, and finally stored users in the session.

We’re almost done with our authentication feature. We need to implement both login and logout functionality, as well as change the layout to include links to those pages.

First things first. Before changing our controllers and views, let’s expose a function that authenticates a given username and password. We will look up a user by username in the database and securely ensure that the user’s password matches the one in the database. The Accounts context is a perfect place to define this function. Open up rumbl/accounts.ex and add the new function authenticate_by_username_and_pass, like this:

 def​ authenticate_by_username_and_pass(username, given_pass) ​do
  user = get_user_by(​username:​ username)
 
 cond​ ​do
  user && Pbkdf2.verify_pass(given_pass, user.password_hash) ->
  {​:ok​, user}
 
  user ->
  {​:error​, ​:unauthorized​}
 
  true ->
  Pbkdf2.no_user_verify()
  {​:error​, ​:not_found​}
 end
 end

We use the existing get_user_by function to look up a User by username. If the user isn’t found, we use comeonin’s no_user_verify() function to simulate a password check with variable timing. This hardens our authentication layer against timing attacks,[19] which is crucial to keeping our application secure. If we find our user and the password matches, we return the user wrapped in an :ok tuple, otherwise we return {:error, :unauthorized} for a bad password, or {:error, :not_found} if the user does not exist for the given username.

Now we are ready to work on our login and logout pages. Let’s add some new routes to lib/rumbl_web/router.ex:

 scope ​"​​/"​, RumblWeb ​do
  pipe_through ​:browser​ ​# Use the default browser stack
 
  get ​"​​/"​, PageController, ​:index
  resources ​"​​/users"​, UserController, ​only:​ [​:index​, ​:show​, ​:new​, ​:create​]
  resources ​"​​/sessions"​, SessionController, ​only:​ [​:new​, ​:create​, ​:delete​]
 end

We add three of the prepackaged REST routes for /sessions. We use the REST routes for GET /sessions/new to show a new session login form, POST /sessions to log in, and DELETE /sessions/:id to log out.

Next, we need a SessionController to handle those actions. Create a lib/rumbl_web/controllers/session_controller.ex, like this:

 defmodule​ RumblWeb.SessionController ​do
 use​ RumblWeb, ​:controller
 
 def​ new(conn, _) ​do
  render(conn, ​"​​new.html"​)
 end
 end

The new action simply renders our login form. We need a second action, create, to handle the form submission, like this:

 def​ create(
  conn,
  %{​"​​session"​ => %{​"​​username"​ => username, ​"​​password"​ => pass}}
 ) ​do
 case​ Rumbl.Accounts.authenticate_by_username_and_pass(username, pass) ​do
  {​:ok​, user} ->
  conn
  |> RumblWeb.Auth.login(user)
  |> put_flash(​:info​, ​"​​Welcome back!"​)
  |> redirect(​to:​ Routes.page_path(conn, ​:index​))
 
  {​:error​, _reason} ->
  conn
  |> put_flash(​:error​, ​"​​Invalid username/password combination"​)
  |> render(​"​​new.html"​)
 end
 end

That create action picks off the inbound arguments for username as username, and for password as pass. Then, we call authenticate_by_username_and_pass. On success, we report a success flash message to the user and redirect to Routes.page_path. Otherwise, we report a failure message to our user and render new again.

Here we can appreciate the benefits of contexts once again. Instead of the controller dealing with all of the complexity, our context handles three return types: {:ok, user}, {:error, :not_found}, and {:error, :unauthorized}. The controller does not care about the details of how authentication works. The controller’s job is to translate whatever our business logic returns into something meaningful for the user, which is quite trivial to do with pattern matching.

In particular, we choose to match only on {:error, _} to ignore the :unauthorized and :not_found reason codes, returning only a vague “Invalid username/password combination” message. We could have returned something like “Invalid password for username” or “Username not found”, but this approach might raise privacy issues as anyone would be able to find whether an email is registered on the website.

We still need to create our view and template. Create a new lib/rumbl_web/views/session_view.ex file that looks like this:

 defmodule​ RumblWeb.SessionView ​do
 use​ RumblWeb, ​:view
 end

Next, we need a session directory for our new view, so create a lib/rumbl_web/templates/session/new.html.eex with our new login form, like this:

 <h1>Login</h1>
 
 <%=​ form_for @conn,
  Routes.session_path(@conn, ​:create​),
  [​as:​ ​:session​],
 fn​ f -> ​%>
  <div>
 <%=​ text_input f, ​:username​, ​placeholder:​ ​"​​Username"​ ​%>
  </div>
  <div>
 <%=​ password_input f, ​:password​, ​placeholder:​ ​"​​Password"​ ​%>
  </div>
 <%=​ submit ​"​​Log in"​ ​%>
 <%​ ​end​ ​%>

We use form_for as in our new-user forms, but instead of passing a changeset, we pass the %Plug.Conn{} struct. Plug.conn structs are useful when you’re creating forms that aren’t backed by a changeset, such as a login or search form. To try out the new page, we have to logout, but we haven’t written that functionality yet. As a temporary workaround, instead of logging out you can clear your browser cookies or start a new session in incognito mode, then visit /sessions/new to try some login attempts.

With a bad login, we see an error flash notice and our template rerendered as shown in the figure.

images/src/authentication/bad_login.png

Now let’s try a good login:

images/src/authentication/good_login.png

It works!

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

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