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:
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.
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:
Accounts
context module: Used to manage user accountsUser
schema: Used to store user account informationUserSessionController
: Used to handle user-related requestsWith 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"
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:
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.
Let’s create an endpoint that uses the JWT to scope access. Here, we can do two things:
"sub"
user id
in the token. This approach is easier because you can leverage the existing authorization you’ve set upWe’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 }
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.
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!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.