Simon Bundgaard-Egeberg I work as a lead developer at a consultancy called it-minds. My primary work has been in React, but I'm now working full-time on an Elixir/Phoenix project.

How to authenticate Phoenix with Guardian

7 min read 1963 104

Authenticate Phoenix Guardian

In this tutorial, we’ll learn how to set up authorization in an Elixir project using Guardian and JWTs. We’ll leverage Phoenix v1.7 and Phoenix LiveView v0.18 to scaffold authentication and authorization code, which will enable us to create a secure way for users to access our app and its resources. Let’s get started!

Jump ahead:

Project overview

We’ll start by creating a module that will serve as an interface between our app and Guardian’s default implementations. We’ll also generate a secret key to sign JWT, as well as an endpoint that returns a JWT when a user provides valid credentials.

Next, we’ll create a new endpoint that uses the JWT to grant access to certain resources. We’ll use the user’s ID to fetch their information and check if they are an admin. If they are, we’ll return additional information that non-admin users won’t have access to.

Throughout this tutorial, we’ll use plug pipelines to fetch user information based on the JWT. This will ensure that only authorized users can access protected resources. By the end of this tutorial, you’ll have a fully functional authorization setup that you can customize to fit the needs of your specific Elixir project.

Getting started

First, let’s set up a new Elixir project with the Phoenix framework, which will provide us with a solid foundation to build our application. Install the necessary dependencies and create a new Phoenix application with the following command:

mix phx.new your_app_name

To serve as a starting point for your application, you can use the phx.gen.auth task, which will generate authentication and authorization code, including the necessary routes and views. Since Phoenix v1.7, this will scaffold LiveView authentication, which is great for our server-side app. In this project, we’d like to expose an API alongside our app, which will require a different kind of authorization, but will use the same table as a user. To generate the code, run the following command from your project’s root directory:

mix phx.gen.auth Accounts User users

The mix task will generate the following files:

  • The Accounts context module: Used to manage user accounts
  • The User schema: Used to store user account information
  • The LiveView templates
  • The UserSessionController: Used to handle user-related requests
  • The user-related routes: Used to define the paths for user authentication and registration

Setting up users and passwords

With that, we’ve set up users and passwords. While I always recommend not handling passwords on your own platform and using services instead, this will serve as a good starting point. So far, our authorization scaffolding adds users, passwords, and a session. We’ll piggyback on this feature, but instead of returning an authorized LiveView session, we‘ll return a JWT with Guardian.

Let’s set that up now. Following Guardian’s Getting Started docs, we’ll first add the Guardian dependency to our mix.exs file:

defp deps do
  [
    ...
    {:guardian, "~> 2.0"},
  ]
end

Then, we’ll create a module that will serve as an authorization central brain:

defmodule LogRocketWeb.Guardian do
  use Guardian, otp_app: :logrocket

  alias LogRocket.Users

  def subject_for_token(user, _claims) do
    {:ok, to_string(user.id)}
  end

  def resource_from_claims(%{"sub" => id}) do
    user = Users.get_user!(id)
    {:ok, user}
  rescue
    Ecto.NoResultsError -> {:error, :resource_not_found}
  end
end

The code above will serve as the interface between our app and the Guardian default implementations. But, before this can work, we need to tell Guardian the secret key it can use to sign the JWT. In the terminal, write the following code:

mix guardian.gen.secret

In turn, this should generate something like the following:

ons1dYYyQjmNEefkIB9rU0cF1aD7ejvYkAcyl9A7H+nfyKNpIMeVTHu2KZUyP9D0

We then set this as our development config in dev.exs in our config folder:

# dev.exs

config :logrocket, LogRocketWeb.Guardian,
  issuer: "logrocket",
  secret_key: "ons1dYYyQjmNEefkIB9rU0cF1aD7ejvYkAcyl9A7H+nfyKNpIMeVTHu2KZUyP9D0"

Create an endpoint

Now, let’s create an endpoint to test our work so far. We’ll call it get_token. get_token will get the user with id = 1 and then call the Guardian API to encode and sign that user into a JWT. We use dbg(jwt) to log the token out to the terminal for us to inspect. Then, we’ll set the response type to application/json and JSON encode our JWT:

defmodule LogRocketWeb.TestController do
  use LogRocketWeb, :controller
  alias LogRocketWeb.Guardian

  def get_token(conn, _params) do
    user = LogRocket.Users.get_user!(1)
    {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user)

    dbg(jwt)
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(200, Jason.encode!(%{token: jwt}))
  end
end

Then, register the route as follows:

 # router.ex
  pipeline :api do
    plug(:accepts, ["json"])
    post("/api/get_token", LogRocketWeb.TestController, :get_token)
  end

Keep in mind that the code above is just a test and not the actual endpoint we’ll use. For now, we just want to confirm that we can indeed create a JWT with the expected information. encode_and_sign\1 will use our application secret to encode a JWT with standard fields listed below.

After this, all that’s left is to use the token and hook it into our session. From there, we have two ways to go. For one, we can modify the example above, take a email and password map, use the existing plumbing to ensure its correctness, and then return the token. But, we’ll get back to that. First, let’s see if our controller returns the first user. You’ll have to create it first via /user/register. Then, triggering the endpoint logs via dbg(jwt) displays the token in the console:

Trigger Endpoint Logs Via JWT Console

This indeed looks like a JWT. Let’s decode it and see what it has in it:

{
  "aud": "logrocket",
  "exp": 1684561851,
  "iat": 1682142651,
  "iss": "logrocket",
  "jti": "0d68a561-5569-472d-9227-02f39350588f",
  "nbf": 1682142650,
  "sub": "1",
  "typ": "access"
}

We see that "sub" is 1, which makes sense, since we’re fetching the user with ID 1. iat and exp note when the token was issued and when it expires. This will default to 30 mins.

Creating a protected endpoint

Let’s create an endpoint that uses the JWT to scope access. Here, we can do two things:

  1. Scope access with claims, which keeps the claims in the token and makes them readily available everywhere the token is used
  2. Manage roles in the application and only keep the "sub" user id in the token. This approach is easier because you can leverage the existing authorization you’ve set up

We’ll create an is_admin key on the user. Although I don’t recommend doing this for applications with real users, it will serve as a good example here because we can scope some access based on this boolean. In that regard, I have two users in my local database. Let’s quickly refactor our get_token function to take a username and a password:

 def get_token(conn, %{"email" => email, "password" => password}) do
    user = LogRocket.Users.get_user_by_email_and_password(email, password)

    case user do
      nil ->
        conn |> put_status(401) |> json(%{error: "Invalid credentials"})

      _ ->
        {:ok, jwt, _full_claims} = LogRocketWeb.Guardian.encode_and_sign(user)
        conn |> put_status(200) |> json(%{token: jwt})
    end
  end

Then, calling the endpoint with the user we created in the UI, we get a token again:

{"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJsb2dyb2NrZXQiLCJleHAiOjE2ODQ1NjM5MTEsImlhdCI6MTY4MjE0NDcxMSwiaXNzIjoibG9ncm9ja2V0IiwianRpIjoiNzllZWQwMDEtODY5MC00ZmFlLWJkNTUtZDJiMDI4MGM3MTMwIiwibmJmIjoxNjgyMTQ0NzEwLCJzdWIiOiIxIiwidHlwIjoiYWNjZXNzIn0.2yEvFhds7nw24d53z_qd286D7jCkpcK4EWxTSQop70N3DNVE6FbvjkBQ0Vcxv_FFoUKyXy4dPUL8H48kA8-3oQ"}

Let’s create a new endpoint that uses this token to fetch a user via the Guardian module we created earlier. Let’s quickly create an endpoint that returns the owner of the token:

  #lib/logrocket_web/controllers/test_controller.ex
  plug(Guardian.Plug.Pipeline, module: LogRocketWeb.Guardian)
  plug(Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"})
  plug(Guardian.Plug.LoadResource, allow_blank: true)

  def me(conn, _params) do
    user = Guardian.Plug.current_resource(conn)

    conn |> put_status(200) |> json(%{email: user.email, id: user.id})
  end


  # lib/logrocket_web/router.ex
  pipeline :api do
    plug(:accepts, ["json"])
    get("/api/get_token", LogRocketWeb.TestController, :get_token)
    get("/api/me", LogRocketWeb.TestController, :me)
  end

Let’s discuss the plug macros on lines 2-4. The first will tell all subsequent plug calls to use our own defined Guardian module, and the next will check if there is a Bearer token prefixed with Bearer. The last will use that Bearer token to fetch a user based on our module, fetching a user with the ID from the sub field in the JWT/Bearer token. Calling the endpoint with the token as a header will give us the following result:

{
        "email": "[email protected]",
        "id": 1
}

Return a secret value

We’re close to goal now. All that’s left is to check whether or not the user is an admin and return a super secret value if they are. We’ll make a quick modification to have the JSON also return the the_answer field, which invokes the get_answer(user) function. get_answer(user) either returns 42 if the user is an admin or unknown if not:

  def me(conn, _params) do
    user = Guardian.Plug.current_resource(conn)

    conn
    |> put_status(200)
    |> json(%{email: user.email, id: user.id, the_answer: get_answer(user)})
  end

  def get_answer(user) do
    if user.is_admin do
      42
    else
      "unknown"
    end
  end

First, we’ll do a test run on our administrative user:

{
        "email": "[email protected]",
        "id": 1,
        "the_answer": 42
}

Then, we’ll test run our basic user:

{
        "email": "[email protected]",
        "id": 2,
        "the_answer": "unknown"
}

Finally, let’s refactor the me function to return a 401 error if no user is found based on the token:

 def me(conn, _params) do
    user = Guardian.Plug.current_resource(conn)

    case user do
      nil ->
        conn |> put_status(401) |> json(%{error: "Invalid credentials"})

      _ ->
        conn
        |> put_status(200)
        |> json(%{email: user.email, id: user.id, the_answer: get_answer(user)})
    end
  end

After setting up our plug pipeline, it is straightforward to retrieve the user based on the token, as demonstrated in the me function. However, to make this even more efficient, you can create a custom Phoenix plug called verify and include it in the API pipeline for every API call. This will parse and validate any token found in the header, making it ready to use in all endpoints.

It’s worth noting that in this implementation, we allow for null users because both the login and me functions use the same plug. If you prefer, you can separate these into two separate pipelines. In that case, if no resource is found in the plug, it will immediately return 401 and never hit the protected controller endpoint.

Conclusion

In this tutorial, we learned how to implement authorization in an Elixir project using Guardian and JWTs. Although we implemented user authentication within the app for demonstration purposes, I recommend leveraging third-party authentication providers like Auth0 to handle authentication. We also learned how to use plug pipelines to fetch user information based on the JWT and how to check if a user is an admin before granting access to protected resources.

With this setup, you can build a secure and scalable app that allows authorized users to access your resources in a controlled and protected manner. I hope you found this tutorial helpful in setting up authorization in your Elixir project. If you have any questions or feedback, please feel free to leave a comment below. Happy coding!

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
Simon Bundgaard-Egeberg I work as a lead developer at a consultancy called it-minds. My primary work has been in React, but I'm now working full-time on an Elixir/Phoenix project.

Leave a Reply