Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Phoenix Authentication

phoenix_authentication.livemd

Phoenix Authentication

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Home Report An Issue Blog: CommentsBlog: Authentication

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • How do we scaffold an authorization system in Phoenix?
  • How do we protect a route from unauthorized access?

Overview

This is a companion reading for the Blog: Authentication exercise. This lesson is an overview of how to add authentication and authorization into a Phoenix application.

Authentication

Authentication is the process of verifying the identity of a user or system. It typically involves providing credentials, such as a username and password or a digital certificate, and validating them against a trusted source.

Authorization

Authorization is the process of determining whether a user or system is allowed to perform a specific action or access a particular resource. It involves verifying the identity and permissions of the requester and comparing them against the access control policies of the system.

Ownership-based Authorization

Some resources should only be managed by the user or groups that own them. For example, in a blog application, a user should probably only be able to edit and delete their own blog posts.

Typically we prove ownership by associating the resource with the user. For example, a user would have many blog posts in a one to many relationship.

Role-based Authorization

Some operations can be allowed based on user roles. For example, an admin user can typically perform all actions that require authorization.

If you’re familiar with the app Discord, then you’re likely very familiar with the role-based authorization system they use to manage permissions in specific voice and text channels.

Password Hashing

Passwords are encrypted in databases to ensure the even if there were a database leak, passwords would be encrypted.

By default, the Phoenix authentication system uses bcrypt_elixir to hash passwords.

Computerphile has a great overview video on hashing algorithms and security.

YouTube.new("https://www.youtube.com/watch?v=b4b8ktEV4Bg&ab_channel=Computerphile")

User Tokens

The Phoenix authentication system uses user tokens to identify users. This avoids storing confidential user information on the client, as that is not secure.

The client sends the token whenever it makes a request, and the server can use this token to identify and retrieve the user from the database. Tokens typically expire after a certain amount of time for security purposes.

Generators

Phoenix provides the following mix phx.gen.auth command to generate all of the scaffolding we need for authentication in our system.

mix phx.gen.auth Accounts User users

The auth system can be generated using controllers, or LiveView. For now, we’ll select n when using the generator to use controllers.

An authentication system can be created in two different ways:
- Using Phoenix.LiveView (default)
- Using Phoenix.Controller only
Do you want to create a LiveView based authentication system? [Yn] n

The generator creates a large number of files, here are a few you should familiarize yourself with:

  • app
    • accounts.ex: The context for managing user accounts.
    • user.ex: The user schema.
    • user_token.ex: The user token schema.
    • user_notifier.ex: The mailer module for user emails such as account confirmation and resetting passwords.
  • app web
    • user_confirmation_controller.ex: controller actions for confirming a user account
    • user_registration_controller.ex: controller actions for user sign up.
    • user_reset_password_controller.ex: controller actions for resetting a user’s password.
    • user_session_controller.ex: controller actions for user log in and log out.
    • user_settings_controller.ex: controller actions for managing user settings.
    • user_auth.ex: Useful functions for managing users on the web side of the application. For example, require_authenticated_user/2 ensures a user is signed in before accessing some resource.
  • tests
    • accounts_fixtures.ex: fixture for creating users in tests.
    • the register_and_log_in_user/1 and log_in_user/2 function in the existing conn_case.ex file.

Modifying Users

The Phoenix generator simply generates the scaffolding for an authentication system. We still own this part of the application and are responsible for maintaining security in our application.

We can also make changes to users, such as adding fields to the users table. For example, we could write a migration that adds a username field or other user information to the users table.

defmodule Blog.Repo.Migrations.AddUsernameToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :username, :string, null: false
    end
  end
end

Associating Resources With A User

In order to associate a resource with a user, it needs to reference a user in the database through a foreign key.

erDiagram

Post {
  string title
  text content
  date published_on
  boolean visibility
  id user_id
}

User {
  string username
  string email
  string password
  string hashed_password
  naive_datetime confirmed_at
}

User ||--O{ Post : "owns"

Here’s an example migration that would add a user_id to an existing posts table.

defmodule Blog.Repo.Migrations.AddUserIdToPosts do
  use Ecto.Migration

  def change do
    alter table(:posts) do
      add :user_id, references(:users, on_delete: :delete_all), null: false
    end
  end
end

Schemas should reflect any changes made in the database.

defmodule Blog.Posts.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :content, :string
    field :title, :string
    field :visible, :boolean, default: true
    field :published_on, :utc_datetime
    has_many :comments, Blog.Comments.Comment
    belongs_to :user, Blog.Accounts.User

    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :content, :visible, :published_on, :user_id])
    |> validate_required([:title, :content, :visible, :user_id])
    |> unique_constraint(:title)
    |> foreign_key_constraint(:user_id)
  end
end

Fixing Broken Tests

Typically making a required field or association in a database table breaks any tests that create those records without the field or association.

To fix tests, it’s necessary to provide the required field or association whenever creating the resource.

Here’s an example of a test that creates a post with an associated user.

test "delete_post/1 deletes the post" do
  user = user_fixture()
  post = post_fixture(user_id: user.id)
  assert {:ok, %Post{}} = Posts.delete_post(post)
  assert_raise Ecto.NoResultsError, fn -> Posts.get_post!(post.id) end
end

Whenever making impactful changes to schema’s/migrations, it’s recommended to make the change when there are no other changes in git. It’s also often best to fix all tests before moving on to other features to avoid potential bugs and unnecessary complexity.

Protecting Controller Actions

The generated UserAuth module contains two useful plugs for protecting routes. The require_authenticated_user/2 plug ensures that a user must be signed in to access the route, and the redirect_if_user_is_authenticated/2 plug does the opposite.

We can use these plugs when we want to protect certain routes.

Requiring Authenticated User

Here’s an example of protecting post controller actions that only an authenticated user should be able to do.

scope "/", BlogWeb do
  pipe_through [:browser, :require_authenticated_user]

  resources "/posts", PostController, only: [:new, :edit, :update, :create, :delete]
end

Requiring User Owns Resource

Sometimes we want to protect a route based on whether or not a user owns the particular resource. For example, we probably only want to let users edit their own posts.

There are multiple ways to require that a user owns a resource. Here are a few options:

  • Individually protect the specific controller actions (handle auth for one action.)
  • Holistically protect several controller actions (handle auth for an entire controller.)
  • Create a custom plug and use it in the router (handle auth for multiple controllers.)

Here’s an example of creating a plug function in a controller that can be selectively applied to multiple routes.

# Typically Goes Above Controller Actions And Below Imports/aliases
plug :require_user_owns_post when action in [:edit, :update, :delete]

# Typically Goes At The Bottom Of The File
defp require_user_owns_post(conn, _params) do
  post_id = String.to_integer(conn.path_params["id"])
  post = Posts.get_post!(post_id)

  if conn.assigns[:current_user].id == post.user_id do
    conn
  else
    conn
    |> put_flash(:error, "You can only edit or delete your own posts.")
    |> redirect(to: ~p"/posts/#{post_id}")
    |> halt()
  end
end

Alternatively, if you wanted to have different error messages for each action, you could include this authorization code in the action itself. This results in more code as each controller action handles it’s own authorization, but it allows for a more specific response.

def delete(conn, %{"id" => id}) do
  if conn.assigns[:current_user].id == post.user_id do
    post = Posts.get_post!(id)
    {:ok, _post} = Posts.delete_post(post)

    conn
    |> put_flash(:info, "Post deleted successfully.")
    |> redirect(to: ~p"/posts")
  else
    conn
    |> put_flash(:error, "You can only delete your own posts.")
    |> redirect(to: ~p"/posts/#{id}")
    |> halt()
  end
end

Testing Flash Messages

Phoenix 1.7 deprecated the get_flash/2 in favor of Phoenix.Flash.get/2. We can use this function in tests if we want to write assertions on flash messages.

Here’s an example of testing that a user cannot edit a resource that doesn’t belong to them.

test "a user cannot edit another user's post", %{conn: conn} do
  post_user = user_fixture()
  other_user = user_fixture()
  post = post_fixture(user_id: post_user.id)
  conn = conn |> log_in_user(other_user) |> get(~p"/posts/#{post}/edit")
  assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "You can only edit or delete your own posts."
  assert redirected_to(conn) == ~p"/posts/#{post}"
end

Route Specificity

When we start splitting up routes into different scopes, it’s possible for routes to conflict.

For example, the following two routes conflict because the wildcard :id would also handle the value "new", so the second route would never trigger.

get "posts/:id", PostController, :show
get "posts/new", PostController, :new

It’s just like having a case statement where the first clause always matches.

url = "/posts/new"

case url do
  "/posts" <> id -> "show route"
  # this case clause will never match.
  "/posts/new" -> "new route"
end

You might encounter weird looking errors like the following if you run into this issue.

Accessing The Current User In The Assigns

By default, if the user is signed in, then the :current_user is available in the conn.assigns. This is set up by the :fetch_current_user plug generated in the router.

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, {BlogWeb.Layouts, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers
  plug :fetch_current_user
end

We can use the @current_user value in any heex template, or provide the @current_user to other function components if necessary.

For example, we can see the @current_user‘s email is displayed in root.html.heex by default.

<li>
  <%= @current_user.email %>
</li>

Including The User Association In Forms

The @current_user stored in the conn.assigns is important if you need to include the current user in a form. Here’s an example of passing the user into a form component.

<.header>
  New Post
  <:subtitle>Use this form to manage post records in your database.


<.post_form changeset={@changeset} action={~p"/posts"} current_user={@current_user} />

<.back navigate={~p"/posts"}>Back to posts

Then the form component can send the user’s id as part of an HTTP request in body params, URL params, or query params. Here’s an example using body params.

<.simple_form :let={f} for={@changeset} action={@action}>
  <.error :if={@changeset.action}>
    Oops, something went wrong! Please check the errors below.
  
  <.input field={f[:user_id]} type="hidden" value={@current_user.id} />
  <.input field={f[:title]} type="text" label="Title" />
  <.input field={f[:content]} type="text" label="Content" />
  <.input field={f[:published_on]} type="datetime-local" label="Publish On" value={DateTime.utc_now()} />
  <.input field={f[:visible]} type="checkbox" label="Visible" />
  <:actions>
    <.button>Save Post
  

However, be mindful that accessing @current_user directly requires that a user must be signed in. If you are on a page where a user doesn’t need to be signed in, then you can access the user safely using the access syntax.

assigns[:current_user]

Signing In A User In Tests

Some tests may require that you sign in a user. Fortunately, the ConnCase module contains a generated log_in_user/2 function we can use to resolve these issues.

Here’s an example of logging in a user for a test.

test "renders form for editing chosen post", %{conn: conn} do
  user = user_fixture()
  post = post_fixture(user_id: user.id)
  # log in the user
  conn = conn |> log_in_user(user) |> get(~p"/posts/#{post}/edit")
  assert html_response(conn, 200) =~ "Edit Post"
end

Hiding Elements

Some elements require authorization or authentication to display on the page.

Here’s an example of hiding an element based on if there is a current user.

<%= if assigns[:current_user] do %>
  Only Authenticated Users Can See This
<% end %>

Here’s an example of hiding an element based on if the user owns a particular resource, in this case a comment.

# It's Important To Verify A Map Exists Before Accessing Fields Such As `id`.
# Otherwise If The Assigns[:current_user] Map Is `nil`, Accessing `id` Of Nil
# Would Cause The Page To Crash.
<%= if assigns[:current_user] &amp;&amp; comment.user_id == assigns[:current_user].id do %>
  <.link href={~p"/comments/#{comment.id}"} method="delete" data-confirm="Are you sure?">
    Delete
  
<% end %>

Preloading Nested Associations

Ecto.Query.preload/3 and Ecto.Repo.preload/3 can preload associated data if the schema includes a has_many/3 or belongs_to/3 association.

It’s also possible to preload nested associations by providing a keyword list to the preload function.

query = from p in Post, preload: [:user, comments: [:user]]

Instead of providing an atom, we can even provide another query modify how the associated results are preloaded.

Here’s an example that preloads post comments and sorts them by newest -> oldest. The comments themselves also preload their associated user.

comments_query = from c in Comment, 
  order_by: [desc: c.inserted_at, desc: c.id],
  preload: :user

post_query = from p in Post, preload: [:user, comments: ^comments_query]

Repo.get!(post_query, id)

Further Reading

We’ve provided an overview of basic route-based and ownership-based authentication. However, authorization is a massive topic far beyond the scope of this course.

There is no single authorization solution, and an effective authorization implementation depends on the application’s needs. Therefore, always consider who should be able to access application resources and how to protect them effectively.

Consider the following resources as you continue to learn more about authorization and authentication:

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Phoenix Authentication reading"
$ git push

We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation

Home Report An Issue Blog: CommentsBlog: Authentication