Adding protected routes and authentication
to a Phoenix
App can have quite a few steps ... ⏳
Luckily, there is a package
(we built; yes, shameless plug =])
that can significantly simplify the process!
In this guide we'll show the steps
for adding auth using
auth_plug
in 5 minutes.
We'll add optional auth
that will let people authenticate
with their GitHub
or Google
Account
and use their Account name and Avatar in the Chat.
Let's do this!
auth_plug
will serve as
"middleware"
for handling authentication.
Add auth_plug
to deps
in mix.exs
, e.g:
{:auth_plug, "~> 1.5"},
then run:
mix deps.get
That will install everything you need.
Visit authdemo.fly.dev,
sign in with your GitHub
or Google
account,
and create your app with localhost:4000
as the URL
:
When you click "Save" you will be see a screen similar to the following:
Note: don't worry this is not a valid key, it's just for illustration purposes.
Copy the shown AUTH_API_KEY
and paste in into
a new .env
file,
e.g:
export AUTH_API_KEY=2cfxNaMmkvwKmHncbYAL58mLZMs/2cfxNa4RnU12gYYSwPvp2hSPFdVDcbdK/authdemo.fly.dev
Make sure the .env
line
is in your .gitignore
file.
Then run:
source .env
This will make the AUTH_API_KEY
environment variable
available.
Note: if you know a better way, let us know!
Save your AUTH_API_KEY
from the Auth App somewhere in your
project directory that can be easily found.
(we will be copying and pasting this into our command prompt).
Please ensure this is still in .gitignore file still!
Next, if you have you phx.server running, close / abort
it (ctrl c
then a
then y
in terminal / cmd).
Now, in command promt in our project directory we're going to set the environment variable manually with:
set AUTH_API_KEY=<YOUR_KEY>
where <YOUR_KEY>
is the API KEY
we just saved from
the Auth App.
Once you make the following changes in the remaing sections,
launch mix phx.server
from the same command prompt
where we just set the AUTH_API_KEY
environment variable
so our application will have access to it.
Open the router.ex
file
and create a new
Optional Auth
pipeline and use it in your routes:
# define the new pipeline using auth_plug
pipeline :authOptional, do: plug(AuthPlugOptional)
scope "/", AppWeb do
pipe_through [:browser, :authOptional]
get "/", PageController, :home
get "/login", AuthController, :login
get "/logout", AuthController, :logout
end
Create a new file:
lib/chat_web/controllers/auth_controller.ex
and add the following code:
defmodule ChatWeb.AuthController do
use ChatWeb, :controller
def login(conn, _params) do
redirect(conn, external: AuthPlug.get_auth_url(conn, "/"))
end
def logout(conn, _params) do
conn
|> AuthPlug.logout()
|> put_status(302)
|> redirect(to: "/")
end
end
The login/2
function redirects to the dwyl auth app.
Read more about how to use the AuthPlug.get_auth_url/2
function.
Once authenticated,
the person will be redirected to the /
endpoint
and a jwt
session is created on the client.
The logout/2
function invokes AuthPlug.logout/1
,
which removes the (JWT) session and redirects back to the homepage.
Create a file with the path:
test/chat_web/controllers/auth_controller_test.exs
Add the following code to it:
defmodule ChatWeb.AuthControllerTest do
use ChatWeb.ConnCase, async: true
test "Logout link displayed when loggedin", %{conn: conn} do
data = %{email: "[email protected]", givenName: "Simon", picture: "this", auth_provider: "GitHub"}
jwt = AuthPlug.Token.generate_jwt!(data)
conn = get(conn, "/?jwt=#{jwt}")
assert html_response(conn, 200) =~ "logout"
end
test "get /logout with valid JWT", %{conn: conn} do
data = %{
email: "[email protected]",
givenName: "Al",
picture: "this",
auth_provider: "GitHub",
sid: 1,
id: 1
}
jwt = AuthPlug.Token.generate_jwt!(data)
conn =
conn
|> put_req_header("authorization", jwt)
|> get("/logout")
assert "/" = redirected_to(conn, 302)
end
test "test login link redirect to authdemo.fly.dev", %{conn: conn} do
conn = get(conn, "/login")
assert redirected_to(conn, 302) =~ "authdemo.fly.dev"
end
end
Now that we've implemented the authentication flow, we need to show it to the person!
Let's first properly show the name
of the logged in person.
Inside lib/chat_web/controllers/page_html.ex
,
add the following function.
def person_name(person) do
person.givenName || person.name || "guest"
end
The HEEX template inside
lib/chat_web/controllers/page_html/home.html.heex
will have access to the functions
inside lib/chat_web/controllers/page_html.ex
,
as it is managed by it.
page_html.ex
is the view
(that is represented by
the files inside page_html/*
),
whereas page_controller.ex
is the controller.
After adding this function,
head over to lib/chat_web/controllers/page_html/home.html.heex
and change it to the following.
<!-- The list of messages will appear here: -->
<ul id='msg-list' phx-update="append" class="pa-1">
</ul>
<footer class="bg-slate-800 p-2 h-[3rem] fixed bottom-0 w-full flex justify-center sticky">
<div class="w-full flex flex-row items-center text-gray-700 focus:outline-none font-normal">
<%= if @loggedin do %>
<input type="text" disabled class="hidden" id="name"
placeholder={person_name(@person)} value={person_name(@person)}
/>
<% else %>
<input type="text" id="name" placeholder="Name" required
class="grow-0 w-1/6 px-1.5 py-1.5"/>
<% end %>
<input type="text" id="msg" placeholder="Your message" required
class="grow w-2/3 mx-1 px-2 py-1.5"/>
<button id="send" class="text-white bold rounded px-3 py-1.5 w-fit
transition-colors duration-150 bg-sky-500 hover:bg-sky-600">
Send
</button>
</div>
</footer>
We are now using the @loggedin
assigns
that is made accessible by auth_plug
to check if a person is logged in.
We are using this property to show the logged in name. If no person is logged in, we show the field in which he can type the wanted name to send messages.
Notice how we use person_name/1
function
we defined in page_html.ex
in this file,
to show the name as placeholder.
Finally,
we need to change the <header>
to show a "Login"
and "Logout"
button.
Inside lib/chat_web/components/layouts/root.html.heex
,
change the <header>
tag to look like so.
<header class="bg-slate-800 w-full h-[4rem] top-0 fixed flex flex-col justify-center z-10">
<div class="flex flex-row justify-center items-center">
<h1 class="w-4/5 md:text-3xl text-center font-mono text-white">
Phoenix Chat Example
</h1>
<div class="float-right mr-3">
<%= if @loggedin do %>
<div class="flex flex-row justify-center items-center">
<img width="42px" src={@person.picture} class="rounded-full"/>
<.link
class= "bg-red-600 text-white rounded px-2 py-2 ml-2 mr-1"
href="/logout"
>
Logout
</.link>
</div>
<% else %>
<.link
class="bg-green-500 text-white rounded px-3 py-2 w-full font-bold"
href="/login"
>
Login
</.link>
<% end %>
</div>
</div>
</header>
We are now checking if any person is logged in.
If it is, we show the profile picture
and a "Logout"
button.
Otherwise, we show a "Login"
button.
These buttons redirect the person
to the /logout
and /login
paths,
respectively,
which are handled by AuthController
we've just created.
And that's it! 🎉 These are all the UI changes we need to make.
In your terminal, run the tests with the following command:
mix test test/chat_web/controllers/auth_controller_test.exs
You should expect to see output similar to the following:
...
Finished in 0.7 seconds (0.7s async, 0.00s sync)
3 tests, 0 failures
Randomized with seed 713921
All tests should pass.
If you run the tests with coverage e.g:
MIX_ENV=test mix coveralls.html
You should see 100%
Coverage :
----------------
COV FILE LINES RELEVANT MISSED
100.0% lib/chat.ex 9 0 0
100.0% lib/chat/message.ex 26 4 0
100.0% lib/chat/repo.ex 5 0 0
100.0% lib/chat_web/channels/room_channel.ex 46 10 0
100.0% lib/chat_web/components/layouts.ex 5 0 0
100.0% lib/chat_web/controllers/auth_controller 14 2 0
100.0% lib/chat_web/controllers/error_html.ex 19 1 0
100.0% lib/chat_web/controllers/error_json.ex 15 1 0
100.0% lib/chat_web/controllers/page_controller 9 1 0
100.0% lib/chat_web/controllers/page_html.ex 9 1 0
100.0% lib/chat_web/endpoint.ex 49 0 0
100.0% lib/chat_web/router.ex 32 5 0
[TOTAL] 100.0%
----------------
Awesome job! 👏
We've just added authentication to our app. It should look like this.