AUTHENTICATE ELIXIR APIS WITH GUARDIAN AND BCRYPT
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.