Home Posts Post Search Tag Search

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.