Authentication

Authentication

https://www.tddphoenix.com/authentication/

** (UndefinedFunctionError) function ChatterWeb.SessionView.render/2 is
undefined (module ChatterWeb.SessionView is not available)

Let's fix that by adding a view module:


 defmodule ChatterWeb.SessionView do use ChatterWeb, :view
end

Run our test again:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
Generated chatter app 05:27:14.210 [error] Server: localhost:4002 (http)
Request: GET /sign_in
** (exit) an exception was raised: ** (Phoenix.Template.UndefinedError) Could not render "new.html" for ChatterWeb.SessionView, please define a matching clause for render/2 or define a template at "lib/chatter_web/templates/session". No templates were compiled for this module.
Assigns: %{conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{layout: {ChatterWeb.LayoutView, "app.html"}}, before_send: [ Assigned keys: [:conn, :view_module, :view_template] (phoenix 1.5.4) lib/phoenix/template.ex:337: Phoenix.Template.raise_template_not_found/3 (phoenix 1.5.4) lib/phoenix/view.ex:310: Phoenix.View.render_within/3 (phoenix 1.5.4) lib/phoenix/view.ex:472: Phoenix.View.render_to_iodata/3 (phoenix 1.5.4) lib/phoenix/controller.ex:776: Phoenix.Controller.render_and_send/4 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible text input or textarea 'Email' but 0, visible text inputs or textareas were found. code: |> sign_in(as: user) stacktrace: (wallaby) lib/wallaby/browser.ex:475: Wallaby.Browser.find/2 (wallaby) lib/wallaby/browser.ex:456: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:17: ChatterWeb.UserCreatesNewChatRoomTest.sign_in/2 test/chatter_web/features/user_creates_new_chat_room_test.exs:9: (test) Finished in 3.6 seconds
1 test, 1 failure

Now we see that there is no "new.html" template:


** (Phoenix.Template.UndefinedError) Could not render "new.html" for
ChatterWeb.SessionView, please define a matching clause for render/2 or define a
template at "lib/chatter_web/templates/session". No templates were compiled for
this module.

You might have expected that error after our last one. Let's just create an
empty template there:


$ mkdir lib/chatter_web/templates/session
$ touch lib/chatter_web/templates/session/new.html.eex

Rerun the test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 1 file (.ex) 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible text input or textarea 'Email' but 0, visible text inputs or textareas were found. code: |> sign_in(as: user) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:17: ChatterWeb.UserCreatesNewChatRoomTest.sign_in/2 test/chatter_web/features/user_creates_new_chat_room_test.exs:9: (test) Finished in 3.6 seconds
1 test, 1 failure

Good! We got rid of all Elixir compilation errors and Phoenix errors. We can now
focus on Wallaby's error — expecting to find forum inputs:


** (Wallaby.QueryError) Expected to find 1, visible text input or textarea
'Email' but 0, visible text inputs or textareas were found.

Let's add a form for our users to sign in:


 <%= form_for @conn, Routes.session_path(@conn, :create), fn f -> %> <label> Email: <%= text_input f, :email %> </label> <label> Password: <%= password_input f, :password %> </label> <%= submit "Sign in" %>
<% end %>

Now rerun our test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 1 file (.ex)
05:42:41.871 [error] Server: localhost:4002 (http)
Request: GET /sign_in
** (exit) an exception was raised: ** (ArgumentError) no action :create for ChatterWeb.Router.Helpers.session_path/2. The following actions/clauses are supported: session_path(conn_or_endpoint, :new, params \\ []) (phoenix 1.5.4) lib/phoenix/router/helpers.ex:374: Phoenix.Router.Helpers.invalid_route_error/3 (chatter 0.1.0) lib/chatter_web/templates/session/new.html.eex:1: ChatterWeb.SessionView."new.html"/1 (phoenix 1.5.4) lib/phoenix/view.ex:310: Phoenix.View.render_within/3 (phoenix 1.5.4) lib/phoenix/view.ex:472: Phoenix.View.render_to_iodata/3 (phoenix 1.5.4) lib/phoenix/controller.ex:776: Phoenix.Controller.render_and_send/4 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible text input or textarea 'Email' but 0, visible text inputs or textareas were found. code: |> sign_in(as: user) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:17: ChatterWeb.UserCreatesNewChatRoomTest.sign_in/2 test/chatter_web/features/user_creates_new_chat_room_test.exs:9: (test) Finished in 3.5 seconds
1 test, 1 failure

The test fails because we do not have a create Routes.session_path. Let's add
that route:


 scope "/", ChatterWeb do pipe_through :browser get "/sign_in", SessionController, :new
resources "/sessions", SessionController, only: [:create] end

Rerun our test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
05:44:22.757 [error] Server: localhost:4002 (http)
Request: POST /sessions
** (exit) an exception was raised: ** (UndefinedFunctionError) function ChatterWeb.SessionController.create/2 is undefined or private (chatter 0.1.0) ChatterWeb.SessionController.create(%Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [ (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 4.0 seconds
1 test, 1 failure

Our test now fails because our controller's create action is undefined: **
(UndefinedFunctionError) function ChatterWeb.SessionController.create/2 is
undefined or private
.


Let's define that action with a modified version of Doorman's example in its
documentation for creating the session:


 def new(conn, _) do render(conn, "new.html") end  def create(conn, %{"email" => email, "password" => password}) do user = Doorman.authenticate(email, password) conn |> Doorman.Login.Session.login(user) |> redirect(to: "/") endend

Rerun the test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
05:45:45.327 [error] Server: localhost:4002 (http)
Request: POST /sessions
** (exit) an exception was raised: ** (RuntimeError) You must add `user_module` to `doorman` in your config Here is an example configuration: config :doorman, repo: MyApp.Repo, secure_with: Doorman.Auth.Bcrypt, user_module: MyApp.User (doorman 0.6.2) lib/doorman.ex:81: Doorman.get_module/1 (doorman 0.6.2) lib/doorman.ex:26: Doorman.authenticate/3 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:9: ChatterWeb.SessionController.create/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 3.9 seconds
1 test, 1 failure

Whoops! This time the test caught an oversight on our part. When adding Doorman,
we did not configure it properly. Thankfully we got a helpful error:


** (exit) an exception was raised: ** (RuntimeError) You must add `user_module` to `doorman` in your config

Here is an example configuration:

config :doorman,
repo: MyApp.Repo,
secure_with: Doorman.Auth.Bcrypt,
user_module: MyApp.User

Let's set that configuration in our config.exs file:


 config :phoenix, :json_library, Jason config :doorman, repo: Chatter.Repo, secure_with: Doorman.Auth.Bcrypt, user_module: Chatter.User

Now rerun our test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 26 files (.ex)
Generated chatter app
05:47:38.978 [error] Server: localhost:4002 (http)
Request: POST /sessions
** (exit) an exception was raised: ** (ArgumentError) Wrong type. The password and hash need to be strings. (comeonin 2.6.0) lib/comeonin/bcrypt.ex:122: Comeonin.Bcrypt.checkpw/2 (doorman 0.6.2) lib/doorman.ex:29: Doorman.authenticate/3 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:9: ChatterWeb.SessionController.create/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 3.9 seconds
1 test, 1 failure

Whoa! That is an unexpected error. The stack trace goes out of our application
and into Doorman's modules and even into Comeonin, a library Doorman uses:


** (ArgumentError) Wrong type. The password and hash need to be strings. (comeonin 2.6.0) lib/comeonin/bcrypt.ex:122: Comeonin.Bcrypt.checkpw/2 (doorman 0.6.2) lib/doorman.ex:29: Doorman.authenticate/3 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:9: ChatterWeb.SessionController.create/2

The error may seem daunting at first. But focusing on the error message, we see
that the password and hash need to be strings. We set the password in our
user factory, so it is a string. But the hashed_password is missing. Doorman
and Comeonin must be trying to compare our password to the missing hashed
version, and that's why we get an error.


We would expect Doorman to set the hashed_password for us. And indeed, looking
at the User module in the documentation's example, we see it import a
hash_password/1 function from Doorman.Auth.Bcrypt. Let's use that function
in our factory.


At this point, I do not know if all of our test users will need a
hashed_password. And since hashing could be slow, I'll create a new function
set_password/2 to do the hashing. Let's change the insert(:user) in our test
to build a user, set and hash the password, and then insert it:


# test/chatter_web/features/user_creates_new_chat_room_test.exs  test "user creates a new chat room successfully", %{session: session} do
- user = insert(:user)
+ user = build(:user) |> set_password("superpass") |> insert()
session |> visit("/")

Now let's add the set_password/2 function to our factory:


  def set_password(user, password) do user |> Ecto.Changeset.change(%{password: password}) |> Doorman.Auth.Bcrypt.hash_password() |> Ecto.Changeset.apply_changes() endend

In set_password/2, we take a User struct and a password. We then turn that
data into a changeset, since the Doorman.Auth.Bcrypt.hash_password/1 function
requires a changeset. Finally, we apply the changes to get a User struct ready
to be inserted into the database.


Now rerun our test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 4.5 seconds
1 test, 1 failure

Alright! We're past the strange error Doorman was throwing. And now, our users
can sign in! But why can't Wallaby find the "New chat room" link?


Our application loses all knowledge of a user's authentication when it redirects
to a new page — we need to use a session.


Setting the current user

Doorman.logged_in?/1
checks for a current_user in the conn.assigns. But we need another plug to
set the current_user in the first place. Doorman's documentation recommends
adding the Doorman.Login.Session plug in our :browser pipeline to do just
that. So let's do it:


 plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers
plug Doorman.Login.Session end

Now, rerun the test:


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
18:54:14.932 [error] Server: localhost:4002 (http)
Request: GET /
** (exit) an exception was raised: ** (ArgumentError) nil given for :session_secret. Comparison with nil is forbidden as it is unsafe. Instead write a query with is_nil/1, for example: is_nil(s.session_secret) (ecto 3.4.6) lib/ecto/query/builder/filter.ex:135: Ecto.Query.Builder.Filter.kw!/7 (ecto 3.4.6) lib/ecto/query/builder/filter.ex:128: Ecto.Query.Builder.Filter.kw!/3 (ecto 3.4.6) lib/ecto/query/builder/filter.ex:110: Ecto.Query.Builder.Filter.filter!/6 (ecto 3.4.6) lib/ecto/query/builder/filter.ex:122: Ecto.Query.Builder.Filter.filter!/7 (ecto 3.4.6) lib/ecto/repo/queryable.ex:70: Ecto.Repo.Queryable.get_by/4 (doorman 0.6.2) lib/login/session.ex:2: Doorman.Login.Session.call/2 (chatter 0.1.0) ChatterWeb.Router.browser/2 (chatter 0.1.0) lib/chatter_web/router.ex:1: ChatterWeb.Router.__pipe_through1__/1 (phoenix 1.5.4) lib/phoenix/router.ex:347: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 4.7 seconds
1 test, 1 failure

The output is large, but the main error is this one:


** (ArgumentError) nil given for :session_secret. Comparison with nil is
forbidden as it is unsafe. Instead write a query with is_nil/1, for example:
is_nil(s.session_secret)

Our :session_secret is nil for some reason. Looking at the stack trace, we
can see it comes from Doorman:


(doorman 0.6.2) lib/login/session.ex:2: Doorman.Login.Session.call/2

Why do we get that error?


If you recall, we defined a session_secret in our users table, but we never
set it in our tests. According to Doorman's documentation, it needs to be set
during user creation with Doorman.Auth.Secret.put_session_secret/1. Let's add
that step to our set_password/2 function to set the session secret when we set
the password:


 def set_password(user, password) do user |> Ecto.Changeset.change(%{password: password}) |> Doorman.Auth.Bcrypt.hash_password()
|> Doorman.Auth.Secret.put_session_secret() |> Ecto.Changeset.apply_changes() end

Now run our test once more!


$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs . Finished in 1.4 seconds
1 test, 0 failures

Great! We did it. Our users can now sign in before they start chatting.


Testing invalid authentication credentials

Before we move further, there's one thing we glossed over in our quest for a
passing feature spec. When I copied over Doorman's example of creating a
session, I called it a modified version because we didn't include any error
handling. Let's do that now.



Read Next page

Report Page