2023-01-27 03:26:14 +13:00
# Getting Started Ash Authentication Phoenix
2023-02-08 03:08:56 +13:00
In this step-by-step tutorial we create a new empty `Example` Phoenix + Ash application which provides the functionality for authentication. For beginners it is the best to follow the tutorial in the given order. For more advanced users it is a good reference to pick and choose from.
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
We assumes that you have [Elixir ](https://elixir-lang.org ) version 1.14.x (check with `elixir -v` ) and Phoenix 1.7 (check with `mix phx.new --version` ) installed. We also assume that you have a [PostgreSQL ](https://www.postgresql.org ) database running which we use to persist the user data.
2023-01-27 03:26:14 +13:00
2023-02-08 03:08:56 +13:00
## Green Field Phoenix Application
2023-01-27 03:26:14 +13:00
2023-02-15 11:33:15 +13:00
We start with a new Phoenix application:
2023-01-30 12:44:31 +13:00
```bash
2023-02-08 03:08:56 +13:00
$ mix phx.new example
2023-01-30 12:44:31 +13:00
$ cd example
```
2023-01-27 03:26:14 +13:00
2023-02-08 03:08:56 +13:00
## Basic Ash Setup
### Application Dependencies
2023-01-30 12:44:31 +13:00
2023-02-15 16:24:01 +13:00
We need to add the following dependencies. Use `mix hex.info dependency_name` to get the latest version of each dependency.
2023-01-30 12:44:31 +13:00
2023-02-07 11:28:00 +13:00
**mix.exs**
2023-01-27 03:26:14 +13:00
```elixir
2023-01-30 12:44:31 +13:00
defmodule Example.MixProject do
use Mix.Project
# ...
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
defp deps do
[
# ...
# add these lines -->
2023-02-15 16:24:01 +13:00
{:ash, "~> x.x"},
{:ash_authentication, "~> x.x"},
{:ash_authentication_phoenix, "~> x.x"},
{:ash_postgres, "~> x.x"}
2023-01-30 12:44:31 +13:00
# < -- add these lines
]
end
2023-01-27 03:26:14 +13:00
# ...
2023-01-30 12:44:31 +13:00
```
Let's fetch everything:
```bash
$ mix deps.get
```
2023-02-08 03:08:56 +13:00
### Formatter
2023-01-30 12:44:31 +13:00
We can make our life easier and the code more consistent by adding formatters to the project. We will use [Elixir's built-in formatter ](https://hexdocs.pm/mix/master/Mix.Tasks.Format.html ) for this.
2023-02-07 11:28:00 +13:00
**.formatter.exs**
2023-01-30 12:44:31 +13:00
```elixir
[
import_deps: [
:phoenix,
# add these lines -->
:ash,
:ash_authentication_phoenix,
:ash_postgres
# < -- add these lines
],
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
2023-01-27 03:26:14 +13:00
]
```
2023-02-08 03:08:56 +13:00
### Phoenix 1.7 compatibility
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
For Phoenix 1.7 we need to change `helpers: false` to `helpers: true` in the router section:
2023-01-27 03:26:14 +13:00
2023-02-07 11:28:00 +13:00
**lib/example_web.ex**
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
```elixir
defmodule ExampleWeb do
# ...
def router do
quote do
use Phoenix.Router, helpers: true # < -- Change this line
# ...
```
2023-02-08 03:08:56 +13:00
### Tailwind
If you plan on using our default [Tailwind ](https://tailwindcss.com/ )-based
components without overriding them you will need to modify your
`assets/tailwind.config.js` to include the `ash_authentication_phoenix`
dependency:
**assets/tailwind.config.js**
```javascript
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
2023-01-30 12:44:31 +13:00
2023-02-08 03:08:56 +13:00
module.exports = {
content: [
"./js/**/*.js",
"../lib/*_web.ex",
"../lib/*_web/**/*.*ex",
"../deps/ash_authentication_phoenix/**/*.ex" // < -- Add this line
],
theme: {
extend: {
colors: {
brand: "#FD4F00",
}
},
},
plugins: [
require("@tailwindcss/forms"),
plugin(({ addVariant }) => addVariant("phx-no-feedback", [".phx-no-feedback& ", ".phx-no-feedback & "])),
plugin(({ addVariant }) => addVariant("phx-click-loading", [".phx-click-loading& ", ".phx-click-loading & "])),
plugin(({ addVariant }) => addVariant("phx-submit-loading", [".phx-submit-loading& ", ".phx-submit-loading & "])),
plugin(({ addVariant }) => addVariant("phx-change-loading", [".phx-change-loading& ", ".phx-change-loading & "]))
]
}
```
## AshPostgres.Repo Setup
We use [AshPostgres ](https://hexdocs.pm/ash_postgres/AshPostgres.html ) to handle the database tables for our application. We need to replace the content of the `Repo` module with the following code:
2023-01-30 12:44:31 +13:00
2023-02-07 11:28:00 +13:00
**lib/example/repo.ex**
2023-01-27 03:26:14 +13:00
```elixir
2023-01-30 12:44:31 +13:00
defmodule Example.Repo do
use AshPostgres.Repo, otp_app: :example
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
def installed_extensions do
["uuid-ossp", "citext"]
end
end
```
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
We have to configure the Repo in `config/config.exs` . While doing that we also configure other stuff which we need later.
2023-02-07 11:28:00 +13:00
**config/config.exs**
2023-01-30 12:44:31 +13:00
```elixir
# ...
import Config
# add these lines -->
config :example,
ash_apis: [Example.Accounts]
config :ash,
:use_all_identities_in_manage_relationship?, false
# <-- add these lines
# ...
```
2023-02-08 03:08:56 +13:00
We need to add `AshAuthentication.Supervisor` to the supervision tree in `lib/example/application.ex` :
2023-01-30 12:44:31 +13:00
`** lib/example/application.ex **`
```elixir
defmodule Example.Application do
# ...
@impl true
def start(_type, _args) do
children = [
# ...
2023-02-08 03:08:56 +13:00
# add this line -->
2023-01-30 12:44:31 +13:00
{AshAuthentication.Supervisor, otp_app: :example}
2023-02-08 03:08:56 +13:00
# < -- add this line
2023-01-30 12:44:31 +13:00
]
# ...
```
2023-02-08 03:08:56 +13:00
## Accounts Api and Resources
2023-01-30 12:44:31 +13:00
2023-02-08 03:08:56 +13:00
We need to create an `Accounts` Api in our application to provide a `User` and a `Token` resource. Strictly speaking we don't need the `Token` resource for just the login with a password. But we'll need it later (e.g. for the password reset) so we just create it now while we are here.
2023-02-07 11:28:00 +13:00
At the end we should have the following directory structure:
2023-01-30 12:44:31 +13:00
```bash
lib/example
├── accounts
│ ├── registry.ex
│ ├── token.ex
│ └── user.ex
├── accounts.ex
...
```
2023-02-07 11:28:00 +13:00
**lib/example/accounts.ex**
2023-01-30 12:44:31 +13:00
```elixir
defmodule Example.Accounts do
use Ash.Api
resources do
registry Example.Accounts.Registry
2023-01-27 03:26:14 +13:00
end
2023-01-30 12:44:31 +13:00
end
```
2023-01-27 03:26:14 +13:00
2023-02-07 11:28:00 +13:00
**lib/example/accounts/user.ex**
2023-01-30 12:44:31 +13:00
```elixir
defmodule Example.Accounts.User do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]
attributes do
uuid_primary_key :id
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
2023-01-27 03:26:14 +13:00
end
2023-01-30 12:44:31 +13:00
authentication do
api Example.Accounts
strategies do
password :password do
identity_field(:email)
end
end
tokens do
enabled?(true)
token_resource(Example.Accounts.Token)
2023-02-07 11:28:00 +13:00
signing_secret(Application.compile_env(:example, ExampleWeb.Endpoint)[:secret_key_base])
2023-01-30 12:44:31 +13:00
end
end
postgres do
table "users"
repo Example.Repo
end
identities do
identity :unique_email, [:email]
2023-01-27 03:26:14 +13:00
end
end
```
2023-02-07 11:28:00 +13:00
**lib/example/accounts/token.ex**
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
```elixir
defmodule Example.Accounts.Token do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource]
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
token do
api Example.Accounts
end
postgres do
table "tokens"
repo Example.Repo
end
end
```
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
Next, let's define our registry:
2023-01-27 03:26:14 +13:00
2023-02-07 11:28:00 +13:00
**lib/example/accounts/registry.ex**
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
```elixir
defmodule Example.Accounts.Registry do
use Ash.Registry, extensions: [Ash.Registry.ResourceValidations]
entries do
entry Example.Accounts.User
entry Example.Accounts.Token
end
end
```
2023-02-08 03:08:56 +13:00
### Create and Migration
2023-01-30 12:44:31 +13:00
2023-02-08 03:08:56 +13:00
Now is a good time to create the database and run the migrations. You have to use specific `ash_postgres` mix tasks for that:
2023-01-30 12:44:31 +13:00
```bash
$ mix ash_postgres.create
$ mix ash_postgres.generate_migrations --name add_user_and_token
$ mix ash_postgres.migrate
```
> In case you want to drop the database and start over again during development you can use `mix ash_postgres.drop` followed by `mix ash_postgres.create` and `mix ash_postgres.migrate`.
2023-02-08 03:08:56 +13:00
## Router Setup
2023-01-30 12:44:31 +13:00
`ash_authentication_phoenix` includes several helper macros which can generate
2023-02-08 03:08:56 +13:00
Phoenix routes for you. For that you need to add 6 lines in the router module or just replace the whole file with the following code:
2023-01-30 12:44:31 +13:00
2023-02-07 11:28:00 +13:00
**lib/example_web/router.ex**
2023-01-30 12:44:31 +13:00
```elixir
defmodule ExampleWeb.Router do
use ExampleWeb, :router
2023-02-08 03:08:56 +13:00
# Add this line
use AshAuthentication.Phoenix.Router
2023-01-30 12:44:31 +13:00
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {ExampleWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
2023-02-08 03:08:56 +13:00
# Add the next line
plug :load_from_session
2023-01-30 12:44:31 +13:00
end
pipeline :api do
plug :accepts, ["json"]
2023-02-08 03:08:56 +13:00
# Add the next line
plug :load_from_bearer
2023-01-30 12:44:31 +13:00
end
scope "/", ExampleWeb do
pipe_through :browser
2023-02-07 11:28:00 +13:00
get "/", PageController, :home
2023-01-30 12:44:31 +13:00
# add these lines -->
sign_in_route()
sign_out_route AuthController
2023-02-08 03:08:56 +13:00
auth_routes_for Example.Accounts.User, to: AuthController
2023-02-07 11:28:00 +13:00
reset_route []
2023-01-30 12:44:31 +13:00
# < -- add these lines
2023-02-08 03:08:56 +13:00
end
2023-01-30 12:44:31 +13:00
2023-02-08 03:08:56 +13:00
# Other scopes may use custom stacks.
# scope "/api", ExampleWeb do
# pipe_through :api
# end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:example, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: ExampleWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
2023-01-30 12:44:31 +13:00
end
2023-02-08 03:08:56 +13:00
end
2023-01-30 12:44:31 +13:00
```
2023-01-27 03:26:14 +13:00
### Generated routes
Given the above configuration you should see the following in your routes:
```
# mix phx.routes
2023-01-30 12:44:31 +13:00
Generated example app
auth_path GET /sign-in AshAuthentication.Phoenix.SignInLive :sign_in
auth_path GET /sign-out ExampleWeb.AuthController :sign_out
auth_path * /auth/user/password/register ExampleWeb.AuthController {:user, :password, :register}
auth_path * /auth/user/password/sign_in ExampleWeb.AuthController {:user, :password, :sign_in}
page_path GET / ExampleWeb.PageController :home
...
2023-01-27 03:26:14 +13:00
```
2023-02-08 03:08:56 +13:00
## AuthController
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
While running `mix phx.routes` you probably saw the warning message that the `ExampleWeb.AuthController.init/1 is undefined` . Let's fix that by creating a new controller:
2023-01-27 03:26:14 +13:00
2023-02-07 11:28:00 +13:00
**lib/my_app_web/controllers/auth_controller.ex**
2023-01-27 03:26:14 +13:00
```elixir
2023-01-30 12:44:31 +13:00
defmodule ExampleWeb.AuthController do
use ExampleWeb, :controller
2023-01-27 03:26:14 +13:00
use AshAuthentication.Phoenix.Controller
def success(conn, _activity, user, _token) do
2023-01-30 12:44:31 +13:00
return_to = get_session(conn, :return_to) || ~p"/"
2023-01-27 03:26:14 +13:00
conn
|> delete_session(:return_to)
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: return_to)
end
def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> render("failure.html")
end
def sign_out(conn, _params) do
2023-01-30 12:44:31 +13:00
return_to = get_session(conn, :return_to) || ~p"/"
2023-01-27 03:26:14 +13:00
conn
|> clear_session()
|> redirect(to: return_to)
end
end
```
2023-02-07 11:28:00 +13:00
**lib/example_web/controllers/auth_html.ex**
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
```elixir
defmodule ExampleWeb.AuthHTML do
use ExampleWeb, :html
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
embed_templates "auth_html/*"
end
```
2023-01-27 03:26:14 +13:00
2023-02-07 11:28:00 +13:00
**lib/example_web/controllers/auth_html/failure.html.heex**
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
```html
< h1 class = "text-2xl" > Authentication Error< / h1 >
```
2023-01-27 03:26:14 +13:00
2023-02-08 03:08:56 +13:00
## Example home.html.heex
2023-01-30 12:44:31 +13:00
To see how the authentication works we replace the default Phoenix `home.html.eex` with a minimal example which has a top navbar. On the right side it shows the `@current_user` and a sign out button. If you are not signed in you will see a sign in button.
2023-02-07 11:28:00 +13:00
**lib/example_web/controllers/page_html/home.html.heex**
2023-01-30 12:44:31 +13:00
```html
< nav class = "bg-gray-800" >
< div class = "px-2 mx-auto max-w-7xl sm:px-6 lg:px-8" >
< div class = "relative flex items-center justify-between h-16" >
< div class = "flex items-center justify-center flex-1 sm:items-stretch sm:justify-start" >
< div class = "block ml-6" >
< div class = "flex space-x-4" >
< div class = "px-3 py-2 text-xl font-medium text-white " >
Ash Demo
< / div >
< / div >
< / div >
< / div >
< div class = "absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0" >
< %= if @current_user do %>
< span class = "px-3 py-2 text-sm font-medium text-white rounded-md" >
< %= @current_user .email %>
< / span >
< a
href="/sign-out"
class="rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70"
>
Sign out
< / a >
< % else %>
< a
href="/sign-in"
class="rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70"
>
Sign In
< / a >
< % end %>
< / div >
< / div >
< / div >
< / nav >
< div class = "py-10" >
< header >
< div class = "px-4 mx-auto max-w-7xl sm:px-6 lg:px-8" >
< h1 class = "text-3xl font-bold leading-tight tracking-tight text-gray-900" > Demo< / h1 >
< / div >
< / header >
< main >
< div class = "mx-auto max-w-7xl sm:px-6 lg:px-8" >
< div class = "px-4 py-8 sm:px-0" >
< div class = "border-4 border-gray-200 border-dashed rounded-lg h-96" > < / div >
< / div >
< / div >
< / main >
< / div >
```
2023-01-27 03:26:14 +13:00
2023-02-08 03:08:56 +13:00
### Start Phoenix
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
You can now start Phoenix and visit
[`localhost:4000` ](http://localhost:4000 ) from your browser.
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
```bash
$ mix phx.server
```
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
### Sign In
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
Visit [`localhost:4000/sign-in` ](http://localhost:4000/sign-in ) from your browser.
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
The sign in page shows a link to register a new account.
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
### Sign Out
2023-01-27 03:26:14 +13:00
2023-01-30 12:44:31 +13:00
Visit [`localhost:4000/sign-out` ](http://localhost:4000/sign-out ) from your browser.
2023-02-07 11:28:00 +13:00
## Reset Password
In this section we add a reset password functionality. Which is triggered by adding `resettable` in the `User` resource. Please replace the `strategies` block in `lib/example/accounts/user.ex` with the following code:
**lib/example/accounts/user.ex**
```elixir
# [...]
strategies do
password :password do
identity_field(:email)
resettable do
sender(Example.Accounts.User.Senders.SendPasswordResetEmail)
end
end
end
# [...]
```
Do make this work we need to create a new module `Example.Accounts.User.Senders.SendPasswordResetEmail` :
**lib/example/accounts/user/senders/send_password_reset_email.ex**
```elixir
defmodule Example.Accounts.User.Senders.SendPasswordResetEmail do
@moduledoc """
Sends a password reset email
"""
use AshAuthentication.Sender
use ExampleWeb, :verified_routes
def send(user, token, _) do
Example.Accounts.Emails.deliver_reset_password_instructions(
user,
url(~p"/password-reset/#{token}")
)
end
end
```
We also need to create a new email template:
**lib/example/accounts/emails.ex**
```elixir
defmodule Example.Accounts.Emails do
@moduledoc """
Delivers emails.
"""
import Swoosh.Email
def deliver_reset_password_instructions(user, url) do
if !url do
raise "Cannot deliver reset instructions without a url"
end
deliver(user.email, "Reset Your Password", """
< html >
< p >
Hi #{user.email},
< / p >
< p >
< a href = "#{url}" > Click here< / a > to reset your password.
< / p >
< p >
If you didn't request this change, please ignore this.
< / p >
< html >
""")
end
# For simplicity, this module simply logs messages to the terminal.
# You should replace it by a proper email or notification tool, such as:
#
# * Swoosh - https://hexdocs.pm/swoosh
# * Bamboo - https://hexdocs.pm/bamboo
#
defp deliver(to, subject, body) do
IO.puts("Sending email to #{to} with subject #{subject} and body #{body}")
new()
|> from({"Zach", "zach@ash-hq.org"}) # TODO: Replace with your email
|> to(to_string(to))
|> subject(subject)
|> put_provider_option(:track_links, "None")
|> html_body(body)
|> Example.Mailer.deliver!()
end
end
```
Your new reset password functionality is active. Visit [`localhost:4000/sign-in` ](http://localhost:4000/sign-in ) with your browser and click on the `Forgot your password?` link to trigger the reset password workflow.