We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Post 77
Ash Framework 06 - Authentication
Published on: 2025-10-07
Tags:
elixir, Blog, Side Project, LiveView, Ecto, Html/CSS, Phoenix, Ash, Framework
Chapter 5: Authentication: Who Are You? (107)
Introducing AshAuthentication
There is 2 part to Authentication for ASH, the core ash_authentication and then the ash_authentication_phoenix. For this chapter we will just be letting the ash installer do most of the work but will be going over each step as is needed. First lets get the right Igniter to install the ash_authentication
mix igniter.install ash_authentication
New Domain, Who's This?
So as before we were working with domains and we had Tunez.Music, Authentication is an other type here that will be used to deal with the users. We just made Tunez.Accounts that will have 2 resources, Tunez.Accounts.User and Tunez.Accounts.Token
Tunez.Accounts.User in lib/tunez/accounts/user.ex is for the Users that we will make and deal with. There is also a DB setup for users in the users database, it doesn't have much at the moment besides ID, and some boilerplate for the configuration
Tokens and Secrets and Config, Oh My!
Tokens, via Tunez.Accounts.Token resource are the secret sauce for AshAuthentication. This is how we will identify a user. They will be provided upon request for every session that a user will need. It should be good right out the box but there are ways to change settings, in the mean time the only thing we need to do now to make this all work is to set up a strategy for the Authentication.
Setting Up Password Authentication
The normal way that most people will use Authentication is to set up a password and user name and then enter those in-order to access the website. There are other ways of doing this like; OAuth and magic links but we will use the password strategy for this site. It is a simple command line to take care of this. This will add in these bits of code:
2 new attributes for Tunez.Accounts.User email and hashed_password
Strategies block added to Authentication config in Tunez.Accounts.User resource
confirmation add-on added to the add_ons block as part of authentication config in Tunez.Accounts.User, this will require the user to authenticate their emails.
A whole set of actions in Tunez.Accounts.Users for signing in.
2 modules to handle sending email confirmations, and password reset.
mix ash_authentication.add_strategy password
With the mix done you will now need to mix ash.migrate to set up the new repo and db
mix ash.migrate
Testing Authentication Actions in iex
One of the actions that we created with the generator is create:register_with_password action. This should take an: email, password, and password_confirmation and creates a user record. Lets test it out.
iex -S mix
iex(1)> Tunez.Accounts.User
Tunez.Accounts.User
iex(2)> |> Ash.Changeset.for_create(:register_with_password, %{email: <<email>>, password: "supersecret", password_confirmation: "supersecret"})
#Ash.Changeset<
domain: Tunez.Accounts,
action_type: :create,
action: :register_with_password,
attributes: %{email: #Ash.CiString<"***email***">},
relationships: %{},
arguments: %{
password: "**redacted**",
email: #Ash.CiString<"***email***">,
password_confirmation: "**redacted**"
},
errors: [],
data: %Tunez.Accounts.User{
id: nil,
email: nil,
confirmed_at: nil,
__meta__: #Ecto.Schema.Metadata<:built, "users">
},
valid?: true
>
iex(3)> |> Ash.create!(authorize?: false)
[debug] QUERY OK db=0.3ms idle=527.3ms
begin []
This did a few things added a new user into the db, created a token for the user to authenticate and confirm their email, generated an email that will be sent out (This wont be done in dev but all the pluming will be there for it.) Now what can we do with this? Lets try to authenticate with a faux form to do so.
iex(1)> Tunez.Accounts.User
Tunez.Accounts.User
iex(2)> |> Ash.Query.for_read(:sign_in_with_password, %{email: "***email***", password: "supersecret"})
#Ash.Query<
resource: Tunez.Accounts.User,
action: :sign_in_with_password,
arguments: %{
password: "**redacted**",
email: #Ash.CiString<"***email***">
},
filter: #Ash.Filter<email == #Ash.CiString<"***email***"> and not is_nil(hashed_password) == "**redacted**">
>
iex(3)> |> Ash.read(authorize?: false)
[debug] QUERY OK source="users" db=1.9ms decode=1.6ms queue=0.8ms idle=765.6ms
SELECT u0."id", u0."email", u0."confirmed_at", u0."hashed_password" FROM "users" AS u0 WHERE (u0."email"::citext = ($1::citext)::citext) AND (NOT (u0."hashed_password"::text IS NULL)) ...
↳ AshPostgres.DataLayer.bulk_create/3, at: lib/data_layer.ex:2024
{:ok,
[
%Tunez.Accounts.User{
id: "cd36f5a7-9f3e-460b-bd45-cdc92367dbfe",
email: #Ash.CiString<"***email***">,
confirmed_at: nil,
__meta__: #Ecto.Schema.Metadata<:loaded, "users">
}
]}
Don't forget each line but you should have the user in the db and then you should be able to find the user with these commands. You can even check the token for the user with the right permissions.
iex(4)> {:ok, [user]} = v()
{:ok,
[
%Tunez.Accounts.User{
id: "cd36f5a7-9f3e-460b-bd45-cdc92367dbfe",
email: #Ash.CiString<"***email***">,
confirmed_at: nil,
__meta__: #Ecto.Schema.Metadata<:loaded, "users">
}
]}
iex(5)> user.__metadata__.token
"eyJhbGciOiJIUzI1NiIsInR5cCI6Ik..."
Now lets test out the verification by use the JSON Wet Token (JWT)
iex(6)> AshAuthentication.Jwt.verify(user.__metadata__.token, :tunez)
[debug] QUERY OK source="tokens" db=0.5ms queue=0.6ms idle=1175.7ms
SELECT TRUE FROM "tokens" AS t0 WHERE (t0."purpose"::text = $1::text) AND (t0."jti"::text = $2::text) LIMIT 1 ...
"purpose" => "user",
"sub" => "user?id=cd36f5a7-9f3e-460b-bd45-cdc92367dbfe"
}, Tunez.Accounts.User}
The interesting part is the purpose and the sub, you can use a JWY's for a variety of reasons for this case we are verifying a user. Now that we have the JWT we can see if a user is using the right JWT and their token.
iex(15)> {:ok, claims, resource} = v()
{:ok, %{...}, Tunez.Accounts.User}
iex(16)> AshAuthentication.subject_to_user(claims["sub"], resource)
SELECT u0."id", u0."confirmed_at", u0."hashed_password", u0."email" FROM
"users" AS u0 WHERE (u0."id"::uuid::uuid = $1::uuid::uuid) [«uuid»]
{:ok, %Tunez.Accounts.User{email: #Ash.CiString<«your email»>, ...}}
You don't need to worry about all of this but again a user will log in and then receive a token that will work for only them.
Automatic UIs with AshAuthenticationPhoenix
Now that we have a way to create a user and then get their token we need a UI for that. We will not use an other igniter to get this done, the command is below and it will:
Set the config file in .igniter.exs this is the first generator that needs a specific configuration
TunezWeb.AuthOverrides module that will customize the look, see (lib/tunez_web/auth_overrides.ex)
TunezWeb.AuthController module to securely process sign-in requests. (in lib/tunez_web/controller)
TunezWeb.LiveUserAuth module providing a set of hooks we can use in live-views (lib/tunez_web/live_user_auth.ex)
updates our router to handle the new plugs and routes (lib/tunez_web/router.ex)
mix igniter.install ash_authentication_phoenix
Okay that is all taken care of but as of right now Igniter doesn't know how to patch JavaScript or CSS files. We need to add Tailwinds live-view paths to the assets. Head to /assets/css/app.css and add in this line
/* ... */
@source "../../lib/tunez_web";
@source "../../deps/ash_authentication_phoenix";
@plugin "@tailwindcss/forms";
/* ... */
Now you can head to /sign-in to see the login page. You can create a user (try the one you already made) you will then be redirected to the home page, we will not work on making it look like you are logged in.
Showing the Currently Authenticated User
It is normal for it to show the current user in the top right so let's do that now. Head to lib/tunez_web/components/layouts.ex and lets add in a line to show the current user.
<div class="flex items-center w-full p-4 pb-2 border-b-2 border-primary-600">
<div class="flex-1 mr-4">
<% # ... %>
</div>
<.user_info current_user={@current_user} socket={@socket} />
</div>
Now we have the line that will show the current user but its not public and we now need to dig into why its not showing the current user.
"Digging into AshAuthenticationPhoenix's Generated Router Code"
Most everything the generator did is perfect but there are a few things that it didn't do for us. It set up some pipelines for load_from_bearer, and load_from_session, these are for the JWT and are able to load the record for the user. But live-view works differently we need to find a way to get the session info into the current session as live-view knows that they are authenticated but not the session that was created for the user.
This is where live_session comes in. It's wrapped in ash with AshAuthentication, ash_authentication_live_session, this will make sure that when new sessions are spawned they will have the correct data and will still be secure. So for Tunez we need to make sure that the routes that need to be logged in will be in the ash_authentication_live_session block. so these first routes need to be moved into the correct block. lib/tunez_web/router.ex
scope "/", TunezWeb do
pipe_through :browser
# This is the block of routes to move
live "/", Artists.IndexLive
# ...
live "/albums/:id/edit", Albums.FormLive, :edit
auth_routes AuthController, Tunez.Accounts.User, path: "/auth"
# ...
# will be moved to this should be higher up in the block of code.
scope "/", TunezWeb do
pipe_through :browser
ash_authentication_live_session :authenticated_routes do
# This is the location that the block of routes should be moved to
live "/", Artists.IndexLive
# ...
live "/albums/:id/edit", Albums.FormLive, :edit
end
end
Stylin' and Profilin' with AuthOverrides
There is a file that was created to change some of the standard components class names and url images etc. Let's head to lib/tunez_web/auth_overrides.ex. It can be hard to figure out exactly what we want to override here. To maybe help the submit button is an input. Let's override the input component. Within the file you will see some syntax for doing some of the overrides.
# add this into the file
override AshAuthentication.Phoenix.Components.Password.Input do
set :submit_class, "bg-primary-600 text-white my-4 py-3 px-5 text-sm"
end
There it is the button is now purple, keep in mind that anything you do here you will change for every instance of the component. They provided a set of overrides in lib/tunez_web/auth_overrides_sample.txt but if don't see it here it is for you.
alias AshAuthentication.Phoenix.Components
override Components.Banner do
set :image_url, nil
set :dark_image_url, nil
set :text_class, "text-8xl text-accent-400"
set :text, "♫"
end
override Components.Password do
set :toggler_class, "flex-none text-primary-600 px-2 first:pl-0 last:pr-0"
end
override Components.Password.Input do
set :field_class, "mt-4"
set :label_class, "block text-sm font-medium leading-6 text-zinc-800"
set :input_class, TunezWeb.CoreComponents.form_input_styles()
set :input_class_with_error, [
TunezWeb.CoreComponents.form_input_styles(),
"!border-error-400 focus:!border-error-600 focus:!ring-error-100"
]
set :submit_class, [
"phx-submit-loading:opacity-75 my-4 py-3 px-5 text-sm",
"bg-primary-600 hover:bg-primary-700 text-white",
"rounded-lg font-medium leading-none cursor-pointer"
]
set :error_ul, "mt-2 flex gap-2 text-sm leading-6 text-error-600"
end
override Components.MagicLink do
set :request_flash_text, "Check your email for a sign-in link!"
end
override Components.MagicLink.Input do
set :submit_class, [
"phx-submit-loading:opacity-75 my-8 mx-auto py-3 px-5 text-sm",
"bg-primary-600 hover:bg-primary-700 text-white",
"rounded-lg font-medium leading-none block cursor-pointer"
]
end
override Components.Confirm.Input do
set :submit_class, [
"phx-submit-loading:opacity-75 my-8 mx-auto py-3 px-5 text-sm",
"bg-primary-600 hover:bg-primary-700 text-white",
"rounded-lg font-medium leading-none block cursor-pointer"
]
end
Why Do Users Always Forget Their Passwords!?
When we did the initial setup for AshAuthentication we did 2 things (senders) that will send emails for confirmation and password reset. SendNewUser ConfirmationEmail SendPasswordResetEmail.
Phoenix apps come with Swoosh that is designed for sending emails. Each sender function does two things: body/1 (generates content) and send/3 that is for constructing and sending the email. There is going to need a email provider to send the emails. However during production you will need to have a place to hold those emails, it will be at http://localhost:4000/dev/mailbox.
Setting Up Magic Link Authentication
Nowadays there is ways to setup some magic links that will just be sent to a user in order to just use the link to log-in. Ash has this in the box. Lets try the magic link method. With the command below it will do the following:
Add a new magic_link authentication strategy to the Tunez.Accounts.User resource, lib/tunez/accounts/user.ex
Add to new actions sign_in_with_magic_link and request_magic_link in Tunez.Accounts.User resource.
Remove allow_nil? false, on the hashed_password in Tunez.Accounts.User resource.
Add a new sender module responsible for generating the magic link email, in lib/tunez/accounts/user/senders/send_magic_link_email.ex
mix ash_authentication.add_strategy magic_link
Then as always migrate
mix ash.migrate
Boom We now have the 2 options.
Debugging When Authentication Goes Wrong
We might need to debug, with more robust error messages during dev. In order to do that we can head to /config/dev.exs and add this to the bottom.
config :ash_authentication, debug_authentication_failures?: true
Restart the server and head to the same magic link and you should see more within the error log, what is it telling you is that the link has already been used. Without the robust logging you might never be able to understand what is happening.
Can We Allow Authentication over Our API?
We can even do all of this over API, First let's set up the command then we can start to lock down the paths over api.
mix ash.extend Tunez.Accounts.User json_api
This made is sure that we have some new actions and then we need to add in a new route with the action register_with_password action. Head to lib/tunez/accounts.ex
defmodule Tunez.Accounts do
use Ash.Domain, otp_app: :tunez, extensions: [AshJsonApi.Domain]
json_api do
routes do
base_route "/users", Tunez.Accounts.User do
post(:register_with_password, route: "/register")
end
end
end
# ...
end
Okay so we have the new route and much more but at the time we can't use the API as there is too much locked down and we don't want people to change peoples password as the API could allow people to do that.