#1 Handle authentication with Phoenix framework
First article of a series about Phoenix and Elixir. This one will be focused on authentication for both HTML and API usage.
Discover the Phoenix Series
#1・Handle HTML & API authentication
#2・Automatic deployment
#3・Pimp my Phoenix
#4・Discord Bot & Stackable Queries
A fresh start
Creating a new web application often means that you will need an authentication logic to handle users and their resources. I offer you today to give a look at Elixir
, Phoenix
framework and phx_auth_gen package to do so.
I will assume that you already know the basics (at least the setup!) of language Elixir and Phoenix framework. If not, I suggest you read the official guide for both of them:
The idea is to create a web application, from scratch, to show you how to handle user logic in both HTML and API ways. And way more! Because as the title of today's article suggests, this is the first one of a series... But for now, let's focus on authentication.
Let's create our project
First, be sure that Elixir
and Phoenix
are installed on your computer. If so, we will create our project by using:
mix phx.new tasty_recipes --binary-id --live
Oh, I forgot to tell you that we will build a website for people who want to share their cooking recipes! Yummy.
The --binary-id
is used to tell Phoenix
that for our models, we don't want simple integers ID but UUIDs instead. Also, --live
means that Phoenix LiveView will be installed and configured in our project. I really advise you to do it now because it can be a little bit tricky to do it later!
Once Phoenix
is done generating all our source code, say yes when asked to fetch and install dependencies. I had some trouble installing nodes modules because of my Node's version. The best way to get everything to work quickly is to be sure that you use Node LTS 14.17.5
(nvm can be helpful here!).
You should now see something like this :
➜ mix phx.new tasty_recipes --binary-id --live
* creating tasty_recipes/config/config.exs
* creating tasty_recipes/config/dev.exs
...
* creating tasty_recipes/assets/static/images/phoenix.png
* creating tasty_recipes/assets/static/robots.txt
Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
* running mix deps.compile
We are almost there! The following steps are missing:
$ cd tasty_recipes
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
Setup database
Now that our project is created, before going further, we need to set up our database. Let's say we are using PostgreSQL.
➜ psql
postgres=# CREATE USER tasty WITH ENCRYPTED PASSWORD 'tasty!';
CREATE ROLE
postgres=# CREATE DATABASE tasty_recipes_dev;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE tasty_recipes_dev TO tasty;
GRANT
Nice, our database is created and we also have a user to interact with it.
We should now update the settings config/dev.exs
of our project to use the correct database and user that we just created :
As the name of the file config/dev.exs
suggests, this is the file where we configure our staging / development environment. There are two other files called prod.exs
and prod.secrets.exs
to handle everything related to production. We will discuss it in another article!
Run server, run!
Now that our database is set up and configured, let's run our server :
➜ mix phx.server
Compiling 13 files (.ex)
Generated tasty_recipes app
[info] Running TastyRecipesWeb.Endpoint with cowboy 2.9.0 at 0.0.0.0:4000 (http)
[info] Access TastyRecipesWeb.Endpoint at http://localhost:4000
webpack is watching the files…
[hardsource:110f7d29] Using 1 MB of disk space.
[hardsource:110f7d29] Writing new cache 110f7d29...
[hardsource:110f7d29] Tracking node dependencies with: package-lock.json.
Hash: 89d1b8f2fdca3182fe0c
Version: webpack 4.46.0
Time: 1215ms
Built at: 08/29/2021 11:05:32 AM
Asset Size Chunks Chunk Names
../css/app.css 11.5 KiB app [emitted] app
../favicon.ico 1.23 KiB [emitted]
../images/phoenix.png 13.6 KiB [emitted]
../robots.txt 202 bytes [emitted]
app.js 353 KiB app [emitted] app
Entrypoint app = ../css/app.css app.js
[0] multi ./js/app.js 28 bytes {app} [built]
[../deps/phoenix/priv/static/phoenix.js] 39.3 KiB {app} [built]
[../deps/phoenix_html/priv/static/phoenix_html.js] 2.21 KiB {app} [built]
[../deps/phoenix_live_view/priv/static/phoenix_live_view.js] 113 KiB {app} [built]
[./css/app.css] 39 bytes {app} [built]
[./js/app.js] 1.43 KiB {app} [built]
+ 3 hidden modules
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!css/app.css:
Entrypoint mini-css-extract-plugin = *
[./node_modules/css-loader/dist/cjs.js!./css/phoenix.css] 10.4 KiB {mini-css-extract-plugin} [built]
[./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./css/app.css] 1.88 KiB {mini-css-extract-plugin} [built]
+ 1 hidden module
You can go to http://localhost:4000/
and see the magic, our website is running!
Create the models
Okay, cool, the server is running, the website is working but what now? It's time to create the models that will handle our users and the TastyRecipes™! As mentioned before and because we are not idiot sandwiches, we will use phx_auth_gen
to take care of it.
First, add it to the list of dependencies in mix.exs
:
Then, install and compile it :
➜ mix do deps.get, deps.compile
Once it is done, let's generate our users' model :
➜ mix phx.gen.auth Accounts User users --binary-id --web html
Planty of fancy things happens here, models are created, templates are updated, new routes are added, etc.--web html
is used to specify to Phoenix
that we want to customize the namespace of our users' module. I like to do it this way to keep the code more readable.
Run the migrations :
➜ mix ecto.migrate
...
12:57:53.846 [info] == Running 20210829103031 TastyRecipes.Repo.Migrations.CreateUsersAuthTables.change/0 forward
12:57:53.848 [info] execute "CREATE EXTENSION IF NOT EXISTS citext"
12:57:53.878 [info] create table users
12:57:53.892 [info] create index users_email_index
12:57:53.897 [info] create table users_tokens
12:57:53.907 [info] create index users_tokens_user_id_index
12:57:53.912 [info] create index users_tokens_context_token_index
12:57:53.917 [info] == Migrated 20210829103031 in 0.0s
If you start again your server, you should see some new links on the index page, Register and Log in. You can navigate through these links and see that all basic features for users are available:
- Sign up
- Email sign up validation
- Sign in
- Forgot password
- Update account settings
- Logout
Since we don't have set up anything to handle emails, if some of them need to be sent, they will simply be printed in the shell where the server is running. Try register and you should see something like this :
Fine, after a few commands, we have a fully working user logic. Awesome!
Yummy! Yummy!
Let's create and migrate our recipes :
➜ mix phx.gen.live Recipes Recipe recipes name description:text owner:references:users --web html
➜ mix ecto.migrate
For the sake of keeping the code and this article simple, we will just say that our recipe consists in having a name
, a description
and a user
that owns the recipe.
Add the new routes to your router :
Note that we addedrequire_authenticated_user
to scope'spipe_through
so only authenticated users can access these routes.
If you visit http://localhost:4000/html/recipes
you should be able to list, show, create, update and delete recipes. That's nice but we have a problem, owner
field is not populated. To overcome this, you need to update the Recipe
changeset and add owner
field to the cast
method:
Assign the currently logged user to the live view socket in mount
so we will be able to use it later:
Pass the currently logged user to the modal where the recipe form is located:
And update the save_recipe
method with action :new
As you can see in theform_component.ex
file, there is two methods calledsave_recipe
with the same number of arguments. What kind of sorcery is this?! It's called pattern matching and if you are not in touch with that, I really advise you to check this awesome article written by Mathieu!
If we save a new recipe again, owner
field should now be populated with the user that created it, nice!
You can check this by updating a little bit the index
template:
And voilà! That's it for the HTML part. We still miss a lot of things for the website to be usable (owner check, design, etc.) but we will see that in the next article, let's focus on API for now!
Oh mighty API, show me your recipes...
For this part, we will use Guardian which is a token-based authentication library for Elixir
applications.
First, add it to the list of dependencies :
Then, install and compile it :
➜ mix do deps.get, deps.compile
Following the Guardian documentation, we have to :
[...] create an "implementation module" which includesGuardian
's functionality and the code for encoding and decoding our token's values. To do this, create a module that usesGuardian
and implements thesubject_for_token/2
andresource_from_claims/1
function :
And update the configuration :
Note: You can generate your ownsecret_key
withmix phx.gen.secret
Let's create the ApiAuthPipeline
and ApiAuthErrorHandler
that we just defined in the configuration.
Cool, we now have all we need to start creating our first API route aka sign_in
:
And of course, we need to create the SessionController
and the view that renders our response:
Everything should be ready now! Let's try it:
➜ http POST localhost:4000/api/sign_in [email protected] password=mypassword
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 485
content-type: application/json; charset=utf-8
date: Sun, 29 Aug 2021 15:01:40 GMT
server: Cowboy
x-request-id: Fp_PGhi_E5gRoL0AABGF
{
"data": {
"email": "[email protected]",
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0YXN0eV9yZWNpcGVzIiwiZXhwIjoxNjMwODU0MTAxLCJpYXQiOjE2MzAyNDkzMDEsImlzcyI6InRhc3R5X3JlY2lwZXMiLCJqdGkiOiI4NjdlOWJmMi04Nzk3LTRjMTQtODdjOS0xODg0ZWVlMmEyNDkiLCJuYmYiOjE2MzAyNDkzMDAsInN1YiI6ImViMDdlYTQ5LTVkYTctNGRiOC04OGFlLWI0YmJjNWIyZmEwZSIsInR5cCI6ImFjY2VzcyJ9.Sa--cLa-6NRvxT7tJGj8LFmND8gUX2NKQ9_2TkbOzZS2YHldQeGYFo6XCtylmtfEVmMAl8lssnzmkjQCEcGnAg"
},
"message": "Successful log in",
"status": "ok"
}
Note: I am using httpie to test the endpoints.
Awesome, looks like we could authenticate ourselves! We now just need to update a little bit our api_authenticated
plug to fetch user when there is an authenticated API call.
Add the fetch_current_user_api
method in the user_auth
file :
Update the api_authenticated
plug :
Now that we can retrieve our JWT token and use it to retrieve the user linked to it, we need to create the routes and logic to interact with the recipes. Thanks to Pheonix
we can generate it on the fly :
➜ mix phx.gen.json Recipes Recipe recipes name description:text owner:references:users --web api --no-ecto --no-context --no-schema
We use --no-ecto
(database migration), --no-context
and --no-schema
because they were already created in the previous steps. You can add the newly created resource to the router :
We will also update the create
method to pass the retrieved user from JWT token as owner
:
Time to try to create a recipe!
➜ http POST http://localhost:4000/api/recipes \
recipe:='{
"name": "Awesome recipe",
"description": "Short but awesome recipe!"
}' \
Authorization:'Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9....'
HTTP/1.1 201 Created
cache-control: max-age=0, private, must-revalidate
content-length: 120
content-type: application/json; charset=utf-8
date: Sun, 29 Aug 2021 16:00:37 GMT
location: /api/recipes/8031b9a3-ba17-4d55-ae8b-06fd27532ed3
server: Cowboy
x-request-id: Fp_SUb19xXArlWAAAAuG
{
"data": {
"description": "Short but awesome recipe!",
"id": "8031b9a3-ba17-4d55-ae8b-06fd27532ed3",
"name": "Awesome recipe"
}
}
And what about retrieving them all?
➜ http http://localhost:4000/api/recipes \
Authorization:'Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9....'
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 736
content-type: application/json; charset=utf-8
date: Sun, 29 Aug 2021 16:01:20 GMT
server: Cowboy
x-request-id: Fp_SW5G1BJQl06MAAAeJ
{
"data": [
{
"description": "Short but awesome recipe!",
"id": "8031b9a3-ba17-4d55-ae8b-06fd27532ed3",
"name": "Awesome recipe"
}
]
}
And that's it! We can now handle authentication & our TastyRecipes™ in both HTML and API way.
You can go further and create more API endpoints for users so they can sign up, update their password and things like that, directly from API, but on our side we will keep it simple!
In the next articles, we will see a few things such as :
- Handle user ownership
- Admin role
- Improve our recipes model
- Implement some real-time features using LiveView
- ...
Don't forget to subscribe if you don't want to miss these updates!
If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.