AUTHENTICATE ELIXIR APIS WITH GUARDIAN AND BCRYPT

Michael Munavu
10 min readJan 8, 2024

--

Authentication in Elixir and Phoenix is as easy as mix phx.gen.auth , which generates for you starter code with sign up , sign in , reset password and change email .

The helper mix phx.gen.auth is very good when building monolithic apps with elixir and phoenix , but when dealing with microservices , where elixir builds the api backend and you have a frontend in either react or vue or one of the million frontend frameworks , then we have to use jwts.

So what are JWTS, short for JSON web tokens , it is an ope standard protocol that defines a self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

Let us break down authentication , what happens when someone logs in or signs up to a system .

Whenever this happens ,a unique token is created and stored in either the local storage or in the cookies , if this token is valid , the system says , I trust this person , show him this page .

Handling JWTS for api authentication will be the same , when we sign up or log in , we will be given a token , we can use this token to access certain routes .

How we do this is by passing this token in the Authorization Header of our http request .

We will see more of how this is done.

Let us get started by creating a new project .

mix phx.new elixir_api_jwt

move to your folder and configure your database in config/dev.exs

Set up your database with

 mix ecto.create

What we first do is create boilerplate code for our Accounts model , our accounts table will have email and hash_password.

We will have this as json.

mix phx.gen.json Accounts Account accounts email hash_password:text

This creates view ,model and controller files for you.

We will configure the routes down below .

Before we migrate , we would want to have unique emails and passwords with a minimum of a certain length .

Let us open our migration files , these will be in priv/repo/migrations

Open the accounts migration file , you should have this

defmodule ElixirApiJwt.Repo.Migrations.CreateAccounts do
use Ecto.Migration

def change do
create table(:accounts) do
add :email, :string
add :hash_password, :text

timestamps()
end
end
end

Add this line at the end of the create block.

create unique_index(:accounts, [:email])

So now we have this

defmodule ElixirApiJwt.Repo.Migrations.CreateAccounts do
use Ecto.Migration

def change do
create table(:accounts) do
add :email, :string
add :hash_password, :text

timestamps()
end

create unique_index(:accounts, [:email])
end


end

Let us also go to our schema files and add these validations

in lib/elixir_api_jwt/accounts/account.ex.

Let us change the changeset function to

 def changeset(account, attrs) do
account
|> cast(attrs, [:email, :hash_password])
|> validate_required([:email, :hash_password])
|> unique_constraint(:email)
|> validate_format(:email, ~r/@/)
|> validate_length(:hash_password, min: 6)
end

USING GUARDIAN AND BCYRPT

HASHING PASSWORDS

Let us get into hashing passwords , hashing is necessary as it hides the real password , that way only the user can know his or her own password .

Before a user is created ,we first need to make sure their password is hashed .

We will be using bcrypt_elixir for this.

Add it to your mix.exs file in the list of dependencies

{:bcrypt_elixir, "~> 3.0"}

And run

mix deps.get

We will now add a few functions to our accounts schema to ensure once a user is created , their password has to be hashed .

Let us look at how it currently works , we will be looking at the create_user function generated for us in lib/elixir_api/jwt

Let us jump into our Iex shell and test it out

iex -S mix
alias ElixirApiJwt.Accounts
Accounts.create_account(%{email: "test@gmail.com" , hash_password: "123456"})
[debug] QUERY OK source="accounts" db=60.1ms decode=4.8ms queue=5.1ms idle=400.6ms
INSERT INTO "accounts" ("email","hash_password","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["test@gmail.com", "123456", ~N[2024-01-08 16:04:19], ~N[2024-01-08 16:04:19]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:376
{:ok,
%ElixirApiJwt.Accounts.Account{
__meta__: #Ecto.Schema.Metadata<:loaded, "accounts">,
id: 1,
email: "test@gmail.com",
hash_password: "123456",
inserted_at: ~N[2024-01-08 16:04:19],
updated_at: ~N[2024-01-08 16:04:19]
}}

As you can see , it gets into our database with the actual password , that is not good , let us work on hashing it .

In our context file , lib/elixir_api_jwt/accounts/accounts.ex.

Add these 2 private functions

 defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{hash_password: hash_password}} = changeset
) do
change(changeset, hash_password: Bcrypt.hash_pwd_salt(hash_password))
end

defp put_password_hash(changeset), do: changeset

Now we will add this function to the changeset function as such

def changeset(account, attrs) do
account
|> cast(attrs, [:email, :hash_password])
|> validate_required([:email, :hash_password])
|> unique_constraint(:email)
|> put_password_hash()
end

Now the whole file looks as

defmodule ElixirApiJwt.Accounts.Account do
use Ecto.Schema
import Ecto.Changeset

schema "accounts" do
field :email, :string
field :hash_password, :string

timestamps()
end

@doc false
def changeset(account, attrs) do
account
|> cast(attrs, [:email, :hash_password])
|> validate_required([:email, :hash_password])
|> unique_constraint(:email)
|> validate_format(:email, ~r/@/)
|> validate_length(:hash_password, min: 6)
|> put_password_hash()
end

defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{hash_password: hash_password}} = changeset
) do
change(changeset, hash_password: Bcrypt.hash_pwd_salt(hash_password))
end

defp put_password_hash(changeset), do: changeset
end

This function now hashes the password for us before getting into the database .

Let us now jump into Iex and try it again

recompile
Accounts.create_account(%{email: "test2@gmail.com" , hash_password: "123456"})
[debug] QUERY OK source="accounts" db=43.6ms queue=3.1ms idle=278.7ms
INSERT INTO "accounts" ("email","hash_password","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["test2@gmail.com", "$2b$12$RJaGpf8Ka5wFA1kJbbogWuvcociL0lJXqsz1DVhGNZqsE1H7W85cO", ~N[2024-01-08 16:10:26], ~N[2024-01-08 16:10:26]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:376
{:ok,
%ElixirApiJwt.Accounts.Account{
__meta__: #Ecto.Schema.Metadata<:loaded, "accounts">,
id: 2,
email: "test2@gmail.com",
hash_password: "$2b$12$RJaGpf8Ka5wFA1kJbbogWuvcociL0lJXqsz1DVhGNZqsE1H7W85cO",
inserted_at: ~N[2024-01-08 16:10:26],
updated_at: ~N[2024-01-08 16:10:26]
}}

Now as you can see , we have

hash_password: "$2b$12$RJaGpf8Ka5wFA1kJbbogWuvcociL0lJXqsz1DVhGNZqsE1H7W85cO",

We have now handled handling.

Let us move into authentication with guardian.

AUTHENTICATION WITH GUARDIAN

Guardian is a token-based authentication library for use with Elixir applications.

We add this to our mix file


{:guardian, "~> 2.0"},

And run

mix deps.get

You can follow the documentation for setting up guardian here

https://github.com/ueberauth/guardian .

In order to leverage Guardian we’ll need first create an “implementation module” which includes Guardian’s functionality and the code for encoding and decoding our token’s values. To do this, create a module that uses Guardian and implements the subject_for_token/2 and resource_from_claims/1 function.

in your lib/elixir_api_jwt folder create a file called guardian.ex and have

defmodule ElixirApiJwt.Guardian do
use Guardian, otp_app: :elixir_api_jwt

def subject_for_token(%{id: id}, _claims) do
# You can use any value for the subject of your token but
# it should be useful in retrieving the resource later, see
# how it being used on `resource_from_claims/1` function.
# A unique `id` is a good subject, a non-unique email address
# is a poor subject.
sub = to_string(id)
{:ok, sub}
end

def subject_for_token(_, _) do
{:error, :reason_for_error}
end

def resource_from_claims(%{"sub" => id}) do
# Here we'll look up our resource from the claims, the subject can be
# found in the `"sub"` key. In above `subject_for_token/2` we returned
# the resource id so here we'll rely on that to look it up.
case ElixirApiJwt.Accounts.get_account!(id) do
nil -> {:error, :reason_for_error}
resource -> {:ok, resource}
end
end

def resource_from_claims(_claims) do
{:error, :reason_for_error}
end
end

Next we need to add our configuration to config/config.exs:

config :my_app, MyApp.Guardian,
issuer: "my_app",
secret_key: "Secret key. You can use `mix guardian.gen.secret` to get one"

so for us , we have

config :elixir_api_jwt, ElixirApiJwt.Guardian,
issuer: "elixir_api_jwt",
secret_key: "Secret key. You can use `mix guardian.gen.secret` to get one"

To generate the secret key run

mix guardian.gen.secret

and replace the string you will be given to the config file.

Let us deal with signing in , when we sign in , what should happen,we should add a user , get a token with the user details back , for this , we will use the encode_and_sign helper by Guardian.

Go to the account controller and edit the create function to

def create(conn, %{"account" => account_params}) do
with {:ok, %Account{} = account} <- Accounts.create_account(account_params),
{:ok, token, _full_claims} <- Guardian.encode_and_sign(account) do
conn
|> put_status(:created)
|> render("account_token.json", account: account, token: token)
end
end

We have added a custom “account_token.json” , that will return to use the account details and the token .

Let us go into the views/account_view.ex and add this function

def render("account_token.json", %{account: account, token: token}) do
%{
id: account.id,
email: account.email,
token: token
}
end

Lastly, we go to our router files and add a route to create and signup a user .

In router.ex have this

scope "/api", ElixirApiJwtWeb do
pipe_through :api
post "/accounts/register", AccountController, :create
end

Now let us try signing up , we fire up our server with

iex -S mix phx.server

Using Postman, we can send a POST request to

http://localhost:4000/api/accounts/register

With the body

{
"account": {

"email": "test3@gmail.com",
"hash_password": "123456"
}
}

We get

{
"id": 3,
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJlbGl4aXJfYXBpX2p3dCIsImV4cCI6MTcwNzE1MDI5NiwiaWF0IjoxNzA0NzMxMDk2LCJpc3MiOiJlbGl4aXJfYXBpX2p3dCIsImp0aSI6IjUyMWU2MDlhLTBmNDAtNGVhZi1iZTdjLWE5MzBhNTJkZWUxMCIsIm5iZiI6MTcwNDczMTA5NSwic3ViIjoiMyIsInR5cCI6ImFjY2VzcyJ9.6wgyr9n0BuBN3Bu3-zQ8NihYG_NigmmunE82GQZo_ThP4HmuteJfmtx0VUjLhOm0bxc4g20NzWy_Zaa7-QwGQQ",
"email": "test3@gmail.com"
}

In the response, we get the token, email, and user id.

Congratulations, you have configured signing up using JWT.

We will now use this token to access authenticated routes and ensure a user is authenticated.

SIGNING IN

Let us now add functions to help in signing in, first, we need to have a way to get a user by their email, in the accounts context file , accounts.ex add this

def get_account_by_email(email) do
Repo.get_by(Account, email: email)
end

In our guardian.ex file add

def authenticate(email, password) do
case ElixirApiJwt.Accounts.get_account_by_email(email) do
nil ->
{:error, :unauthorized}

resource ->
case validate_password(password, resource.hash_password) do
true -> create_token(resource)
false -> {:error, :reason_for_error}
end
end
end

def validate_password(password, hash_password) do
Bcrypt.verify_pass(password, hash_password)
end

defp create_token(account) do
{:ok, token, _full_claims} =
encode_and_sign(account)

{:ok, account, token}
end

We will be authenticating a user using email and their password .

We get the account first via email and use Bycpt to verify whether the password we passed “123456”, is similar to the one hashed in our database .

We will use these functions now for signing in , let us go to our account controller and add

  def sign_in(conn, %{"account" => %{"email" => email, "hash_password" => hash_password}}) do
case ElixirApiJwt.Guardian.authenticate(email, hash_password) do
{:ok, account, token} ->
conn
|> put_status(:ok)
|> render("account_token.json", account: account, token: token)

{:error, _reason} ->
conn
|> put_status(:unauthorized)
|> render("error.json", error: "invalid credentials")
end
end

We check if the user exists and if the password is the same, then we render the user’s id, email and token.

We. have defined “error.json”, let us add that in the views/account_view.ex

 def render("error.json", %{error: error}) do
%{
error: error
}
end

Let us add a route in router.ex , in the “/api” scope, we add

   post "/accounts/sign_in", AccountController, :sign_in

So we have


# Other scopes may use custom stacks.
scope "/api", ElixirApiJwtWeb do
pipe_through :api
post "/accounts/register", AccountController, :create
post "/accounts/sign_in", AccountController, :sign_in
end

Let us test this out with postman

we are sending a post request to

http://localhost:4000/api/accounts/sign_in

with the body

{
"account": {

"email": "test3@gmail.com",
"hash_password": "123456"

}
}

And we get this body back

{
"id": 3,
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJlbGl4aXJfYXBpX2p3dCIsImV4cCI6MTcwNzE1MjM5OSwiaWF0IjoxNzA0NzMzMTk5LCJpc3MiOiJlbGl4aXJfYXBpX2p3dCIsImp0aSI6ImJjYmFhYzc4LTNlNzEtNGQ3NS1hMzBjLWYzZmFhYWIxYjI1OCIsIm5iZiI6MTcwNDczMzE5OCwic3ViIjoiMyIsInR5cCI6ImFjY2VzcyJ9.Tgd6hXKN-c-NEx5Tt69m_iOOKqxBCtgX1Ju_baDzBQRuilQ9UKyl-FtDLXMztFP5Y07p2Ol2JrFIvov5XymlXg",
"email": "test3@gmail.com"
}

If we pass in an invalid password or email, we get

{
"error": "invalid credentials"
}

RESET PASSWORD .

When resetting the password, we want to ensure the user passed in the current_password and the new hash_password .

Let us add a reset_password function in the account controller.

def reset_password(conn, %{"account" => account_params}) do
case Accounts.get_account_by_email(account_params["email"]) do
nil ->
conn
|> put_status(:unauthorized)
|> render("error.json", error: "Email not found")

account ->
case ElixirApiJwt.Guardian.validate_password(account_params["current_password"], account.hash_password) do
true ->
with {:ok, %Account{} = account} <- Accounts.update_account(account, account_params),
{:ok, token, _full_claims} <- ElixirApiJwt.Guardian.encode_and_sign(account) do
render(conn, "account_token.json", account: account, token: token)
end

false ->
conn
|> put_status(:unauthorized)
|> render("error.json", error: "invalid credentials")
end
end
end

And in our router file, add

 patch "/accounts/reset_password", AccountController, :reset_password

Let us test this out in postman

We send a patch request to

http://localhost:4000/api/accounts/reset_password

With the body

{
"account": {


"email": "test4@gmail.com",
"current_password": "123456",
"hash_password": "new password"

}
}

And getting back

{
"id": 4,
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJlbGl4aXJfYXBpX2p3dCIsImV4cCI6MTcwNzE1MzMyMCwiaWF0IjoxNzA0NzM0MTIwLCJpc3MiOiJlbGl4aXJfYXBpX2p3dCIsImp0aSI6ImNiZDQ4ZWExLTliNmEtNDkyNi05N2NmLTljMzFlMDMzZGMxOSIsIm5iZiI6MTcwNDczNDExOSwic3ViIjoiNCIsInR5cCI6ImFjY2VzcyJ9.5gcbwoyu8vWJBvUo2TqV2tT5XL7dSmlQB1jqguOyqhdPedGWTZaj_5prWEk3S0Fqr-Lkiewjs_Tc3qKzAx1QKw",
"email": "test4@gmail.com"
}

if we pass an invalid email, we get

{
"error": "Email not found"
}

And if we pass in the wrong current_password , we get

{
"error": "invalid credentials"
}

Now we are good for resetting passwords.

PROTECTING CERTAIN ROUTES

If we want to ensure that a user is logged in to ensure we have the right people seeing the right things, we have to make sure they are protected.

This can be found in the guardian documentation, the first thing we do is create an error handler.

Let us first create a file called guardian_error_handler.ex in lib/elixir_api_jwt

and add

defmodule ElixirApiJwt.GuardianErrorHandler do
import Plug.Conn

def auth_error(conn, {type, _reason}, _opts) do
body = Jason.encode!(%{message: to_string(type)})

conn
|> put_resp_content_type("application/json")
|> send_resp(401, body)
end
end

Let us then create a pipeline, guardian_pipeline.ex

, and add

defmodule ElixirApiJwt.GuardianPipeline do
use Guardian.Plug.Pipeline,
otp_app: :elixir_api_jwt,
module: ElixirApiJwt.Guardian,
error_handler: ElixirApiJwt.GuardianErrorHandler

plug Guardian.Plug.VerifyHeader
plug Guardian.Plug.VerifySession
plug Guardian.Plug.EnsureAuthenticated
plug Guardian.Plug.LoadResource
end

Finally, let us add configurations to the router.ex file .

Add

  pipeline :auth do
plug ElixirApiJwt.GuardianPipeline
end

Then a scope, we want to ensure only authenticated people can see all users

 scope "/api", ElixirApiJwtWeb do
pipe_through [:api, :auth]
get "/accounts", AccountController, :index
end

Let us now try accessing the “/accounts”

send a GET request to

http://localhost:4000/api/accounts

We get,

{
"message": "unauthenticated"
}

HOW TO ACCESS PROTECTED ROUTES .

We access protected routes by passing the Authorization in the header.

We are passing it as “Bearer #{token}”, we get this token after signing up or signing in, you can add that here

Now when we add a token here, we get

We are now good, we can access certain routes after authorization.

I hope you have got a good understanding of elixir api authentication with jwts .

You can view the source code here

Thank you so much for taking the time to learn with me, you can reach out to me at https://michaelmunavu.com in case of any questions.

--

--